Where to Hold React Component Data: state, store, static, and this

One of the most important concepts to master when building applications with React is how to effectively manage component data. Choosing the right place to store information can make your components more reusable, maintainable, and performant.

React offers several options for holding component data, each designed for specific use cases. These include local component state, the Redux global store, static class properties, the this keyword, and even module-scoped variables. Knowing when to use each approach is key to designing robust React architecture.

In this article, we‘ll take a deep dive into the different data storage options available in React components. We‘ll examine the pros and cons of each approach and consider common use cases through practical examples. By the end, you‘ll have a solid framework for deciding where to store data in your own React projects. Let‘s get started!

Local Component State

Undoubtedly the most common way to store data in React components is through the use of local component state. Whenever you define a state object within a component (either with a constructor function or the useState hook), you‘re creating local state.

Here‘s a simple example of a functional component that uses the useState hook to manage local state:

import React, { useState } from ‘react‘;

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

In this example, count is a piece of local component state. It‘s specific to each instance of the Counter component. If you rendered three components in your app, each would have its own internal count state.

The most important thing to remember about local component state is that updating it triggers a re-render of the component and its children. This is what keeps your UI in sync with your data. In the example above, clicking the button increments count, which triggers a re-render to display the updated click count.

Local state is very useful for storing ephemeral UI state like form input values, toggles, accordion expand/collapse states, and more. Essentially, if a piece of data is specific to a single component instance and changes to it should be visually reflected in the component, local state is a good choice.

However, local state is not a good place to store data that needs to be shared across multiple components. If two components both need access to the same piece of data, it‘s better to "lift" that state up into a common parent component or into a global state management solution like the Redux store.

Redux Store

For more complex React applications, it‘s common to manage data in a global Redux store outside of your component hierarchy. The Redux store acts as a "single source of truth" for your application state.

Components access the Redux store data they care about with selector functions, usually defined with the useSelector hook:

import React from ‘react‘;
import { useSelector } from ‘react-redux‘;

function UserProfile() {
  const user = useSelector(state => state.user);

  return (
    <div>

      <p>Email: {user.email}</p>
    </div>
  );
}

Here, the UserProfile component retrieves the current user data from the Redux store with the useSelector hook. Any time the user data is updated in the store (usually through a dispatched action), the useSelector hook will trigger a re-render of the UserProfile with the latest data.

The Redux store is a great place to keep data that‘s considered "global" to your application. Common examples include:

  • User authentication state (e.g. the current logged-in user and their permissions)
  • Important data fetched from APIs that‘s used on many pages
  • Application configuration and settings
  • Cross-cutting concerns like showing error messages toasts or modal dialogs

By moving this kind of data out of your local component state and into Redux, you make it accessible to any component that needs it without having to manually pass it down through multiple layers of the component tree.

However, putting too much data in your Redux store can slow performance, so be judicious. Stick to data that‘s truly global, and let components manage their own ephemeral UI state locally. You can always refactor local state into Redux later if needed.

Static Class Properties

React class components can define static properties that are shared across all instances of the component. Static properties are not instance-specific, so they don‘t have access to this and cannot read or update the component‘s props or state.

Static properties also don‘t trigger re-renders when they‘re updated, because they‘re not part of the component‘s state or props.

Here‘s an example of a component that defines a static property:

class MyComponent extends React.Component {
  static defaultColor = ‘blue‘;
  render() {
    return <div style={{ backgroundColor: MyComponent.defaultColor }}>Hello</div>;
  }
}

In this example, defaultColor is a static property. It‘s accessed using the class name MyComponent rather than this. Changing defaultColor would not trigger a re-render.

Static properties are useful for holding configuration data that‘s consistent across all instances of a component. They provide a convenient way to set default values that can be overridden by props:

class Button extends React.Component {
  static defaultProps = {    
    color: ‘blue‘,
    size: ‘medium‘
  };
  render() {
    const { color, size } = this.props;
    return <button className={`btn ${color} ${size}`}>{this.props.children}</button>;
  }
}

Here, the Button component defines default color and size values as a static defaultProps object. These defaults will be used if no color or size prop is specified when the Button is used:

<Button>Click me</Button> // Renders a medium blue button
<Button color="red">Click me</Button> // Renders a medium red button 

Static properties are also a good place to define utility methods that don‘t rely on any instance-specific data. For example:

class MathUtils extends React.Component {
  static multiply(a, b) {
    return a * b;
  }
  static add(a, b) {
    return a + b;  
  }
}

console.log(MathUtils.add(1, 2)); // 3
console.log(MathUtils.multiply(3, 3)); // 9

Overall, static properties are a handy tool for defining configuration and utility methods, but they‘re not used for holding data that varies between component instances or that should trigger re-renders when changed.

The this keyword

In React class components, the this keyword lets you attach arbitrary properties to a component instance. These properties are not part of React‘s component model, so they don‘t trigger re-renders when changed, but they can still be useful in some cases.

Here‘s an example of attaching an instance property with this:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myProperty = 42;
  }
  render() {
    return <div>{this.myProperty}</div>;
  }
}

Here, myProperty is attached to the component instance with this.myProperty. It can be read in render() or other instance methods, but updating it won‘t cause a re-render.

A more common use case for this is for storing references to child components or DOM nodes, which you can do with the ref attribute:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.inputRef = React.createRef();
  }
  render() {
    return <input ref={this.inputRef} />;  
  }
  componentDidMount() {
    this.inputRef.current.focus();
  }
}

This example uses a ref to store a reference to the rendered element, which we can then use to programatically call focus() on it after it mounts.

While this can be used to store arbitrary instance properties, it‘s not recommended, as it can make it harder to reason about what data a component relies on. Stick to refs for the most part.

Module-Scoped Variables

Finally, it‘s worth noting that you can store data in plain JavaScript variables declared in the same module as a component. This is not a React-specific technique, but it‘s worth considering as you decide where to store component-related data.

Here‘s an example:

let backgroundColor = ‘blue‘;

function MyComponent() {  
  return <div style={{ backgroundColor }}>Hello</div>;
}

function changeColor() {
  backgroundColor = ‘red‘;
}

In this example, backgroundColor is declared as a module-level variable. It‘s used by the MyComponent function to set the background color of the rendered

. The changeColor function can update backgroundColor, which would affect any components that use it. However, this would not automatically trigger a re-render.

In general, it‘s best to avoid module-scoped variables for holding component data. If the data is specific to a single component, use local state. If it needs to be shared across many components, move it into the Redux store instead. Keeping data in global variables can make it harder to reason about what‘s happening as your application grows.

Only use module-scoped variables for constants or configuration that truly doesn‘t change over the lifetime of the application.

Conclusion

As we‘ve seen, React provides several options for storing component data, each with its own strengths and use cases. To summarize:

  • Use local component state for data that‘s specific to a single component instance and for ephemeral UI state. Updating local state will trigger a re-render of the component.

  • Use the Redux store for global application data that‘s used across many parts of your app. Components retrieve this data with selector functions and useSelector, and it‘s updated by dispatching actions.

  • Use static class properties for cross-instance configuration data and utility methods. Static properties don‘t have access to this and can‘t see instance-specific data.

  • Use this to store instance properties like refs, but avoid it for other data.

  • Only use module-scoped variables for constants and fixed configuration. Avoid storing changeable component data at the module level.

There‘s no one-size-fits-all approach, but considering the criteria of whether data should be global or instance-specific, and whether changes should trigger re-renders or not, will help guide you to the right choice.

Taking the time to thoughtfully consider where each piece of component data belongs can keep your components maintainable, performant, and reusable in the long run. Happy coding!

Similar Posts