These are the concepts you should know in React.js (after you learn the basics)

You‘ve gone through the tutorials, learned the fundamentals, and built a few simple React apps. But to really step up your React game, there are some key concepts you‘ll want to dive into next. As a full-stack developer who has worked extensively with React, I‘m going to share the top 5 concepts that will take your React skills and knowledge to the next level.

1. Component Lifecycle In-Depth

One of the most important things to understand in React is the component lifecycle – that is, the stages and hooks that a component goes through from birth to death. Having a solid grasp of the lifecycle will allow you to effectively initialize components, handle updates, make API calls at the right times, and avoid memory leaks.

The lifecycle is divided into three main phases:

Mounting

This is the phase when a component is first created and inserted into the DOM. The key lifecycle methods involved in mounting are:

  • constructor(): Initializes state, binds methods. Not a place for side effects like API calls.

  • render(): Required. Returns the component JSX to be rendered in the DOM. Should be pure (same input always gives same output).

  • componentDidMount(): Invoked right after first render. Good place to initialize 3rd party libraries, start animations, or load async data from an API.

Here‘s an example component that logs a message when it mounts:

class ExampleComponent extends React.Component {
  componentDidMount() {
    console.log(‘Component has mounted!‘);
    // Initialize a library or make API call here
  }

  render() {
    return <div>Hello World</div>;
  }
}

Updating

After mounting, a component will enter the updating phase anytime there is a change to its state or props. Key methods:

  • shouldComponentUpdate(nextProps, nextState): Allows you to prevent unnecessary re-renders by returning false if a re-render is not required.

  • render(): Same as before, renders the updated component DOM.

  • componentDidUpdate(prevProps, prevState): Invoked right after update. Can make API calls based on change in props. Be careful not to setState and trigger infinite loop!

componentDidUpdate(prevProps) {
  // Typical usage, don‘t forget to compare props!
  if (this.props.userID !== prevProps.userID) {
    this.fetchData(this.props.userID);
  }
}

Unmounting

The final phase of the lifecycle is when a component is removed from the DOM.

  • componentWillUnmount(): Called right before component unmounts. Use this to clean up timers, cancel network requests, or remove event listeners to prevent memory leaks.
componentWillUnmount() {
  // Clean up subscriptions
  ChatAPI.unsubscribeFromFriendStatus(
    this.props.friend.id,
    this.handleStatusChange
  );
}

By understanding each lifecycle method, when it‘s invoked, and its use cases, you‘ll be able to tap into the full power of class components. Of course with React Hooks, functional components can now have state and lifecycle too, but more on that later.

2. Leveraging Higher-Order Components

Let‘s talk about one of the most useful techniques for code reuse and logic sharing between React components – higher-order components, or HOCs. An HOC is simply a function that takes a component as an argument and returns a new, enhanced component.

const EnhancedComponent = higherOrderComponent(OriginalComponent);

The returned component will render the original wrapped component, but with some added capabilities or data. This pattern allows you to keep your components focused and reuse functionality across your app.

One common use case for HOCs is authentication. Let‘s say you have certain pages or actions that should only be accessible to logged-in users. Rather than checking auth state and redirecting in each individual component, you could create an HOC like this:

function withAuth(WrappedComponent) {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        isAuthenticated: false
      };
    }

    componentDidMount() {
      // Check authenticated status when component mounts
      this.checkAuth();
    }

    checkAuth = () => {
      // API call or check token to set auth state
      const isAuthenticated = AuthService.isAuthenticated();
      this.setState({ isAuthenticated });
    }

    render() {
      if (this.state.isAuthenticated) {
        return <WrappedComponent {...this.props} />;
      } else {
        return <Redirect to="/login" />;
      }
    }
  };
}

Now you could wrap any component that needs auth protection:

const PrivateDashboard = withAuth(Dashboard);

The PrivateDashboard component will now only render if the user is authenticated, otherwise they‘ll be redirected to the login page. You could apply this to multiple components while keeping the auth logic in one place.

Some other ideas for HOCs:

  • Loading state for fetching and displaying data
  • Error handling
  • Styling or layout
  • Connecting to a data store like Redux

HOCs let you enhance components while keeping them decoupled. It‘s a powerful technique to have in your React toolbox.

3. Mastering setState and React State

State is one of the key concepts that makes React components dynamic and interactive. Whether it‘s form inputs, toggle switches, or API data, state gives components memory and allows them to change over time in response to user events and other stimuli.

To modify a component‘s state in React, you use the setState method. This is the only legitimate way to update state – you should never try to modify the state object directly. There are a few important things to know about how setState works.

First, it‘s important to note that setState is asynchronous. This means the state won‘t be updated immediately after you call setState, and the component won‘t re-render right away. If you try to access the state object immediately after, you may not get the latest values:

// Wrong!
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // Hasn‘t updated yet

To perform some action after the state has been updated, you can pass a callback function as the second argument to setState:

this.setState(
  { count: this.state.count + 1 },
  () => { console.log(this.state.count); } // New count value
)

The callback will be executed once the state has been updated and the component re-rendered.

Another key aspect of setState is that it performs a shallow merge of the new state object you provide into the existing state. This means any values you don‘t specify will remain unchanged. It also means that when setting nested objects, you may inadvertently overwrite other properties at that level.

// Initial state
this.state = {
  user: {
    name: ‘John‘,
    age: 30
  }
};

// Attempting to update
this.setState({
  user: {
    age: 31    
  }
});

console.log(this.state);
// { 
//   user: {
//     age: 31
//   }
// }

// The name property was overwritten!

To avoid this, you should use the object spread syntax to merge the updated values:

this.setState(prevState => ({
  user: {
    ...prevState.user,
    age: 31    
  }
}));

Note that here we‘re using the alternate form of setState that takes a function rather than an object. When the new state depends on the previous state or props, this is the recommended approach to ensure you‘re accessing the most current values (since setState calls may be batched).

// Relying on existing state - not guaranteed to work! 
this.setState({
  count: this.state.count + 1
});
this.setState({
  count: this.state.count + 1
});

// Instead, use functional setState
this.setState((prevState, props) => ({
  count: prevState.count + 1
}));
this.setState((prevState, props) => ({
  count: prevState.count + 1
}));

By understanding the asynchronous nature of setState, how it merges updates, and when to use the functional form, you‘ll be able to predictably manage state in your components and avoid common pitfalls.

4. Utilizing the Context API

Have you ever experienced the pain of prop drilling? You want to pass some data down to a deeply nested component, so you‘re stuck passing props through multiple levels of intermediate components that don‘t even need it. There‘s a component, say a theme switcher or authenticated user, that many components need access to, but it doesn‘t make sense to put its state in some far removed common ancestor. This is where React‘s Context API comes to the rescue.

Context provides a way to share values between components without having to pass props manually at every level. You can think of it as global state for a component tree. Here are the basic steps to use Context:

  1. Create a Context object with a default value:
const ThemeContext = React.createContext(‘light‘);
  1. Provide the Context value to the component tree:
<ThemeContext.Provider value="dark">
  <App />
</ThemeContext.Provider>
  1. Consume the Context value in any component in the tree, even if it‘s deeply nested:
class ThemedButton extends React.Component {
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

The contextType class property is one way to access the Context value in a class component. For functional components, you can use the useContext hook:

const theme = useContext(ThemeContext);

One thing to keep in mind is that whenever the Provider‘s value prop changes, any components that consume that Context will re-render. So if you‘re using Context to store frequently updated values, it may cause performance issues. Typically, Context is best suited for data that needs to be accessible by many components at different levels, like theme, locale, or authenticated user.

Here‘s a more complete example of using Context to share an authenticated user object across an app:

// Create the Context
const UserContext = React.createContext();

// Provider component
function UserProvider(props) {
  const [user, setUser] = useState(null);

  const login = (userData) => {
    setUser(userData);
  };

  const logout = () => {
    setUser(null);
  };

  return (
    <UserContext.Provider value={{ user, login, logout }}>
      {props.children}
    </UserContext.Provider>
  );
}

// Consuming components
function Avatar() {
  const { user } = useContext(UserContext);
  if (!user) {
    return <AnonAvatar />;
  }
  return <UserAvatar user={user} />;
}

function LoginButton() {
  const { login } = useContext(UserContext);
  return <Button onClick={login}>Log in</Button>;
}

function LogoutButton() {
  const { logout } = useContext(UserContext);
  return <Button onClick={logout}>Log out</Button>;
}

// App component
function App() {
  return (
    <UserProvider>
      <Avatar />
      <LoginButton />
      <LogoutButton />
    </UserProvider>
  );
}

With the Context API, you can neatly sidestep prop drilling and make your React code cleaner and more maintainable.

5. Staying Updated with React

My last bit of advice for taking your React skills to the next level is to keep up with new releases and features. The React landscape is always evolving, and staying current will help you write better, more idiomatic code and take advantage of performance and developer experience improvements.

Some relatively recent additions that are important to know about:

  • React Hooks: Hooks, introduced in React 16.8, let you use state and other React features in functional components, without writing a class. This has led to a shift towards preferring functional components over class components. Understanding hooks like useState, useEffect, useContext, and useRef will help you write more concise and reusable code.

  • Concurrent Mode: Still experimental, Concurrent Mode is a set of new features that help React apps stay responsive and gracefully adjust to the user‘s device capabilities and network speed. Features include Suspense for data fetching, and useTransition/useDeferredValue for smoother UI updates.

  • Server Components: Currently in development, Server Components will allow developers to build apps that span the server and client, combining the rich interactivity of client-side apps with the improved performance of server rendering.

To stay in the loop, I recommend following the official React blog and Twitter account, as well as keeping an eye on the documentation. The React community is also very active, with many great blogs, tutorials, and conference talks that can help you deepen your understanding.

Of course, the best way to really learn and internalize these concepts is to build things with them! As you come across new APIs and patterns, try integrating them into your projects. Tinker with them, see how they impact performance and code structure. Don‘t be afraid to make mistakes – that‘s how we learn and grow as developers.

Conclusion

We‘ve covered a lot of ground in this post, from component lifecycle and higher-order components to Context and the latest React features. I hope diving into these concepts has given you a clearer understanding of React‘s power and flexibility, and equipped you with some new tools for your developer toolbelt.

Remember that learning React, or any technology, is an ongoing journey. You don‘t have to understand every concept or use every feature right away. Focus on solidifying the fundamentals, then gradually explore and incorporate more advanced techniques.

As long as you keep coding, keep learning from the community and official sources, and don‘t shy away from challenging yourself, you‘ll be well on your way to React mastery. Happy coding!

Similar Posts