Managing State with React Hooks - Without Redux and Context API

Original author: André Gardi
  • Transfer
Hello! My name is Arthur, I work on VKontakte as a mobile web team, I am engaged in the VKUI project - a library of React-components, with the help of which some of our interfaces in mobile applications are written. The issue of working with a global state is still open for us. There are several well-known approaches: Redux, MobX, Context API. I recently came across an article by André Gardi State Management with React Hooks - No Redux or Context API , in which the author suggests using React Hooks to control the state of the application.

Hooks are rapidly breaking into the lives of developers, offering new ways to solve or rethink different tasks and approaches. They change our understanding of not only how to describe components, but also how to work with data. Read the translation of the article and the commentary of the translator under the cat.

image

React Hooks are More Powerful Than You Think


Today we will study React Hooks and develop a custom hook for managing the global state of the application, which will be simpler than the Redux implementation and more productive than the Context API.

React Hooks Basics


You can skip this part if you are already familiar with hooks.

useState ()


Before the appearance of hooks, functional components did not have the ability to set a local state. The situation has changed with the advent useState().



This call returns an array. Its first element is a variable that provides access to the state value. The second element is a function that updates the state and redraws the component to reflect the changes.

import React, { useState } from 'react';
function Example() {
  const [state, setState] = useState({counter:0});
  const add1ToCounter = () => {
    const newCounterValue = state.counter + 1;
    setState({ counter: newCounterValue});
  }
  return (
    

You clicked {state.counter} times

); }

useEffect ()


Class components respond to side effects using lifecycle methods such as componentDidMount(). A hook useEffect()allows you to do the same in functional components.

By default, effects are triggered after each redraw. But you can make sure that they are executed only after changing the values ​​of specific variables, passing them the second optional parameter in the form of an array.

// Вызов без второго параметра
useEffect(() => {
  console.log('Я буду запускаться после каждого рендера');
});
// Со вторым параметром
useEffect(() => {
  console.log('Я вызовусь только при изменении valueA');
}, [valueA]);

To achieve a similar result, componentDidMount()we will pass an empty array to the second parameter. Since the contents of an empty array always remain unchanged, the effect will be executed only once.

// Вызов с пустым массивом
useEffect(() => {
  console.log('Я запущусь только первый раз');
}, []);

State sharing


We saw that a hook state works just like a class component state. Each component instance has its own internal state.

To share the state between the components, we will create our own hook.



The idea is to create an array of listeners and only one state. Every time a component changes state, all subscribed components call their own getState()and are updated due to this.

We can achieve this by calling useState()inside our custom hook. But instead of returning the function setState(), we will add it to the array of listeners and return the function that updates the state object inside and calls all the listeners.

Wait a moment. How does it make my life easier?


Yes you are right. I created an NPM package that encapsulates all the described logic.

You do not have to implement it in every project. If you no longer want to spend time reading and want to see the final result, just add this package to your application.

npm install -s use-global-hook

To understand how to work with a package, study examples in the documentation. And now I propose to focus on how the package is arranged inside.

First version


import { useState, useEffect } from 'react';
let listeners = [];
let state = { counter: 0 };
const setState = (newState) => {
  state = { ...state, ...newState };
  listeners.forEach((listener) => {
    listener(state);
  });
};
const useCustom = () => {
  const newListener = useState()[1];
  useEffect(() => {
    listeners.push(newListener);
  }, []);
  return [state, setState];
};
export default useCustom;

Use in component


import React from 'react';
import useCustom from './customHook';
const Counter = () => {
  const [globalState, setGlobalState] = useCustom();
  const add1Global = () => {
    const newCounterValue = globalState.counter + 1;
    setGlobalState({ counter: newCounterValue });
  };
  return (
    

counter: {globalState.counter}

); }; export default Counter;

This version already provides sharing state. You can add an arbitrary number of counters to your application, and they will all have a common global state.

But we can do better


What do you want:

  • remove the listener from the array when unmounting the component;
  • make the hook more abstract to use in other projects;
  • manage initialStateusing parameters;
  • rewrite the hook in a more functional style.

Calling a function just before unmounting a component


We already found out that calling useEffect(function, [])with an empty array works the same way componentDidMount(). But if the function passed in the first parameter returns another function, then the second function will be called right before unmounting the component. Just like componentWillUnmount().

So, in the code of the second function, you can write the logic for removing a component from an array of listeners.

const useCustom = () => {
  const newListener = useState()[1];
  useEffect(() => {
    // Вызывается сразу после монтирования
    listeners.push(newListener);
    return () => {
      // Вызывается прямо перед размонтированием
      listeners = listeners.filter(listener => listener !== newListener);
    };
  }, []);
  return [state, setState];
};

Second version


In addition to this update, we also plan:

  • pass React parameter and get rid of import;
  • export not customHook, but a function that returns customHook with the given initalState;
  • create an object storethat will contain a value stateand a function setState();
  • replace arrow functions with ordinary ones in setState()and useCustom()so that you can associate storewith this.

function setState(newState) {
  this.state = { ...this.state, ...newState };
  this.listeners.forEach((listener) => {
    listener(this.state);
  });
}
function useCustom(React) {
  const newListener = React.useState()[1];
  React.useEffect(() => {
    // Вызывается сразу после монтирования
    this.listeners.push(newListener);
    return () => {
      // Вызывается прямо перед размонтированием
      this.listeners = this.listeners.filter(listener => listener !== newListener);
    };
  }, []);
  return [this.state, this.setState];
}
const useGlobalHook = (React, initialState) => {
  const store = { state: initialState, listeners: [] };
  store.setState = setState.bind(store);
  return useCustom.bind(store, React);
};
export default useGlobalHook;

Separate actions from components


If you've ever worked with complex state management libraries, then you know that manipulating a global state from components is not a good idea.

It would be more correct to separate the business logic by creating actions for changing the state. Therefore, I want the latest version of the package to provide components with access not to setState(), but to a set of actions.

To do this, we provide our useGlobalHook(React, initialState, actions)third argument. Just want to add a couple of comments.

  • Actions will have access to store. In this way, actions will be able to read the contents store.state, update the state with a call, store.setState()and even call other actions by accessing store.actions.
  • To avoid mess, the action object may contain subobjects. Thus, you can move actions.addToCounter(amount) in a sub-object with all ekshenom counter: actions.counter.add(amount).

Final version


The following snippet is the current version of the NPM package use-global-hook.

function setState(newState) {
  this.state = { ...this.state, ...newState };
  this.listeners.forEach((listener) => {
    listener(this.state);
  });
}
function useCustom(React) {
  const newListener = React.useState()[1];
  React.useEffect(() => {
    this.listeners.push(newListener);
    return () => {
      this.listeners = this.listeners.filter(listener => listener !== newListener);
    };
  }, []);
  return [this.state, this.actions];
}
function associateActions(store, actions) {
  const associatedActions = {};
  Object.keys(actions).forEach((key) => {
    if (typeof actions[key] === 'function') {
      associatedActions[key] = actions[key].bind(null, store);
    }
    if (typeof actions[key] === 'object') {
      associatedActions[key] = associateActions(store, actions[key]);
    }
  });
  return associatedActions;
}
const useGlobalHook = (React, initialState, actions) => {
  const store = { state: initialState, listeners: [] };
  store.setState = setState.bind(store);
  store.actions = associateActions(store, actions);
  return useCustom.bind(store, React);
};
export default useGlobalHook;

Examples of using


You no longer have to deal with useGlobalHook.js. Now you can focus on your application. The following are two examples of using the package.

Multiple Counters, One Value


Add as many counters as you want: they will all have a global value. Each time one of the counters will increment the global state, all the others will be redrawn. In this case, the parent component does not need redrawing.
Living example .

Asynchronous ajax requests


Search GitHub repositories by username. We process ajax requests asynchronously using async / await. We update the query counter with each new search.
Living example .

That's it


We now have our own state management library on React Hooks.

Translator Commentary


Most existing solutions are essentially separate libraries. In this sense, the approach described by the author is interesting in that it uses only the built-in React features. In addition, compared to the same Context API, which also comes out of the box, this approach reduces the number of unnecessary redraws and therefore wins in performance.

Also popular now: