useState and useEffect in React

Since React’s move to using hooks instead of classes, two of the primary hooks we need to get used to using are useState and useEffect.

Note: I’m using Typescript for my React apps. so code listed will have types etc. but if you’re from a pure Javascript background it should still be pretty obvious what’s going on.

useState

So, in the time before hooks, we might use classes. We’d pass props into the constructor of the class and set state within the class. Hence, we might end up with something like this

interface IViewerProps {
   startAt: number;
}

interface IViewerState {
    counter: number;
}

export class Viewer extends React.Component<IViewerProps, IViewerState> {
   constructor(props: IViewerProps) {
      super(props);
         this.state = {
            counter:  props.startAt
         }

      this.onClick = this.onClick.bind(this);
   }

   onClick(): void {
      this.setState({
         counter: this.state.counter + 1
      });
   }

   render() {
      return (
         <div>
            <div>{this.state.counter}</div>
            <button onClick={this.onClick}>Click Me</button>
         </div>
      );
   };
}

In this instance we’d pass a startAt value via the props, assign to the internal state then update this internal state.

Functional Components

Now, the move to functional based components ofcourse would lose the ability to maintain state across function calls, unless we had some way to associate it with that function call. Whereas a class would handle this initialization within its constructor. In the case of a functional component, we need a way where subsequent calls to that function cannot reinitialize the state or – in the case of out button counter example, the state would simply be reset to the one supplied by the props each render.

Let’s look at recreating the class above but as a functional component.

export function Viewer(props: IViewerProps) {
   const [state, setState] = useState(props.startAt);

   function onClick(): void {
      setState(state + 1);
   }

   return (
      <div>
         <div>{state}</div>
            <button onClick={onClick}>Click Me</button>
         </div>
   );
}

Now in this case useState is initialized the first time it’s used with the props.startAt value. This initialization does not take place again, during the lifecycle of this function, so that when you click the button it updates the state and re-renders the component without reinitializing the state. We can see this by putting console.log(`Props: ${props.startAt} State: ${state}`); after the useState line. In this case you’ll see the props value remains constant but the state changes on each click of the button.

This is great. But, what happens if the parent control actually needs to change the props. So, for example maybe we click a reset button to reset the value to the default.

useEffect

Whilst useState allows us to store state between function calls on a React component, we need a way to handle side effects, or more specifically in this example, we need ways of changing the state when the props change.

Let’s assume our parent component can set and reset the initial state for our Viewer component via the props. In fact, here’s that App component to demonstrate this


function App() {
   const [state, setState] = useState(1);

   function onReset() {
      setState(state === 0 ? 1 : 0);
   }

   return (
      <div className="App">
         <header className="App-header">
            <Viewer startAt={state} onReset={onReset}/>
         </header>
      </div>
   );
}

Note: this is a silly contrived example as we need the props to actually change – but for real work usage, imagine at some point a change in your app. is stored to localStorage and maybe. onReset loads the latest from localStorage. If that props value has now changed it will not (at this time) be reflected in the Viewer render.

You can see we’re using useState to supply the state as props to our Viewer component from our App. If you load the app as it stands, you’ll see nothing changes on the page, from our original implementation. The Viewer will keep incrementing even when reset is clicked. This is because we have no way to reset the state (remember it’s created like it would be in a constructor, i.e. when the function was first called).

This is where we use useEffect. The useEffect hook allows us to respond to changes in the props (and/or other dependencies), by adding the following code below the useState line in the Viewer component

useEffect(() => {
   setState(props.startAt);
}, [props])

Now when the props change (the [props] code denotes useEffecthas a dependency on the props value) useEffect will call setState, updating it with the latest props. We could ofcourse make this more granular by just having a dependency on [props.startAt]. We can supply an array of dependencies, any of which changes will cause useEffect code to execute.

Note: Ofcourse with a React class-based component we will also have the issue of how to reinitialize state from the props, because the props are set via the constructor. Hence this is not an issue just for functional components but in these cases useEffect is an elegant solution.