DEV Community

FilippoFilip
FilippoFilip

Posted on

I replaced useState hook with custom one

React hooks are already released since React 16.8. Most of developers fell in love with this "new" API but also small part of them still prefer to write components in good "old" class way.

I started to use this API almost from their release and I also would consider myself as a fan of hooks. So I'm on bright side, it means that writing components without classes make the code very readable. It could also lead to less code, that means at the end of the day - less bugs.

Fast coding

React team has done great work and their hooks API are covering every use case what developer may need when building beautiful React applications. However when building React applications with hooks I mostly use just basic hooks like: useState, useEffect and useRef and in more complex components also useContext, useReducer and useMemo come in handy.

Ok so get down to bussines and let's see some code 😍!

Initially, when I was using useState hook I found myself declaring many primitive state variables in single component.

For simplicity let's consider example of controlled form with few inputs like this:



function App() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [phone, setPhone] = useState("");

  return (
    <form>
      <label>Name</label>
      <input type="text" value={name} onChange={e => setName(e.target.value)} />
      <label>Email</label>
      <input
        type="text"
        value={email}
        onChange={e => setEmail(e.target.value)}
      />
      <label>Phone</label>
      <input
        type="text"
        value={phone}
        onChange={e => setPhone(e.target.value)}
      />
    </form>
  );
}

export default App;


Enter fullscreen mode Exit fullscreen mode

Form above is very simple example of basic hooks usage of state. Each field is stated in separate variable created by hook and this value is controlled by separate setter function.

OK but, what's wrong with it ?
I would say nothing :) Let's try to go back in time and have a look on the same component but in class approach like this:



class App extends Component {
  state = {
    name: "",
    email: "",
    phone: ""
  };

  render() {
    return (
      <form>
        <label>Name</label>
        <input
          type="text"
          value={this.state.name}
          onChange={e => this.setState({ name: e.target.value })}
        />
        <label>Email</label>
        <input
          type="text"
          value={this.state.email}
          onChange={e => this.setState({ email: e.target.value })}
        />
        <label>Phone</label>
        <input
          type="text"
          value={this.state.phone}
          onChange={e => this.setState({ phone: e.target.value })}
        />
      </form>
    );
  }
}

export default App;


Enter fullscreen mode Exit fullscreen mode

As you can see it's very similar and there is no special difference between them, just using class keyword and render method, right? But there is also one thing that for me was in this approach more convenient way of making components.

Yes it's the controlling of the state by only one function this.setState and accessing the state by just one property this.state. This small little thing was great on class components and I missed this so much in functional world of React.

Are you asking why?
Let's say you are coding component of which state is not very clear at the beginning and you are adding, renaming or deleting state properties on the go while coding.

For instance in the case of adding state properties I'd need to define new variables with their own names and setter functions. In return I'd need to access value and use separate setter for controlling state of hook. In case of definition of more state values it gives to component very fractional and repetition look, especially with const and useState keywords.

Diff of adding some new state values could look like this:



  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [phone, setPhone] = useState("");
+  const [address, setAddress] = useState("");
+  const [bio, setBio] = useState("");


Enter fullscreen mode Exit fullscreen mode

If we consider this to be just the definition of component state with only five properties. In my opinion there is just a lot of repetition looking code, especially when comparing to class state definitions shown below.



  state = {
    name: "",
    email: "",
    phone: "",
+   address: "",
+   bio: "",
  };


Enter fullscreen mode Exit fullscreen mode

In this case the state definition has clear and very understandable structure, without repetition code. Main game changer for me is that every state property can be accessed from one place and set by one function.
Redux guys may say it's like single source of truth for the one component. That's what I like about it.

enter image description here
Declaring many variables and accessing them were puzzling me for quite long time until I came up with simple idea of custom hook called useSetState.

Take a glance on it in action bellow.



function App() {
  const [state, setState] = useSetState({
    name: "",
    email: "",
    phone: ""
  });

  return (
    <form>
      <label>Name</label>
      <input
        type="text"
        value={state.name}
        onChange={e => setState({ name: e.target.value })}
      />
      <label>Email</label>
      <input
        type="text"
        value={state.email}
        onChange={e => setState({ email: e.target.value })}
      />
      <label>Phone</label>
      <input
        type="text"
        value={state.phone}
        onChange={e => setState({ phone: e.target.value })}
      />
    </form>
  );
}

export default App;


Enter fullscreen mode Exit fullscreen mode

Wow 🤩! Just one hook for whole state? Controlled from one place? Without using class ? That's pretty neat !

Since the moment of definition this custom hook I just stopped using regular useState (I'm lying... I use it for super simple components f.e. togglers etc.) and I started to use it every time I need to store some state.

This hook just brings to my code:

  • less repetitive look
  • better flexibility of accessing and controlling state
  • easier handling of state changes
  • advantages of class state in functional component.

Are you wondering how this custom hook looks like 🤔 ?



const useSetState = (initialState = {}) => {
  const [state, regularSetState] = useState(initialState);

  const setState = newState => {
    regularSetState(prevState => ({
      ...prevState,
      ...newState
    }));
  };

  return [state, setState];
};


Enter fullscreen mode Exit fullscreen mode

It's just simple custom hook with regular useState hook. The useState hook holds object by default. Then in defined setState function is calling regular setter function called regularSetState, but instead of assigning primitive variables to state, its assigning merged object of previous state object and new state object. This behavior leads to opportunity to store many values in one state object and to be set by one function. So simple but so powerful.

enter image description here

Conclusion

For sure, I'm not the first who adjusted useState hook like this. Maybe it's not even the right way to handle it, but it just works for me very well and i like it. Maybe it would work for you too.

Top comments (9)

Collapse
 
dance2die profile image
Sung M. Kim

I tend to implement such a hook with useReducer after reading @leewarrickjr's awesome article,

You will see that, with useReducer, you specify "how" to handle state changes.

So you can write a merge function as a reducer and pass the initial state as shown below.

const merge = (oldState, newState) => ({ ...oldState, ...newState });
const initialState = { name: "", email: "", phone: "" };

export default function App() {
  const [state, setState] = React.useReducer(merge, initialState);

  return (
    <div className="App">
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The working code is below. (check the console output after submit).

Collapse
 
leewarrickjr profile image
Lee Warrick

Nice work!

Collapse
 
dance2die profile image
Sung M. Kim

Thanks, mate~! and for the post!

Collapse
 
abdelrahmanahmed profile image
Wahdan • Edited

Question from my side, why you are not using something like that :

let [state, setState] = useState({
    name: "",
    email: "",
    phone: "",
})
Enter fullscreen mode Exit fullscreen mode

and whenever yo want to update the state, for example lets update the name field :

setState(
...state,
{
name:"wahdan"
})
Enter fullscreen mode Exit fullscreen mode

even with const it will behave the same

Collapse
 
filippofilip95 profile image
FilippoFilip • Edited

Hi, thanks for your comment :) Your code solves the same problem as I'm describing in article, only without using custom hook.

I would definitely also use your implementation but still:

  • Direct destructing of previous state in setState function could happen to be repeated in many event handlers in your component. So you will need to type ...state for many times in one component.
  • Extracting that "object merging" to separate custom hook could maybe in shortest possible way look like:
function useSetState(initialValue) {
    const [state, setState] = useState(initialValue);

    return [state, (newState) => setState({
        ...state, ...newState,
    })];
}
Enter fullscreen mode Exit fullscreen mode

and than to be used like:

setState({ name: "John" }
Enter fullscreen mode Exit fullscreen mode

If you import this hook into many of your components, than this way still gives you option to avoid destructing of previous state like ...state as i mentioned above.

  • Becouse of that it just worth it to have it as custom hook :D
Collapse
 
munawwar profile image
Munawwar

Yea.. aside from it not looking nice to always repeat the same thing, there is the issue of forgetting to spread when doing the setState .. and that results in overriding the entire object and other bugs.

Collapse
 
munawwar profile image
Munawwar

I did the same. My hook's named useObjectState: The first two values of return array it's same like useState.. state and setState. I expect object as input (not intended for other data types).

The third one is spreadState - it spreads the new state object with the old one at first level of the object.

The fourth one is mergeState - that does a deep merge of the new state with the old one. Helps with nested states.

This way I am able to remain sane when dealing with large number of states. The use case is same as yours - for large form UIs.

Collapse
 
shrijan00003 profile image
Shrijan

Awesome, I have tried a similar one for updating state if the component is mounted in typescript-react,

code looks like:

const useStateMounted = <T,>(initialValue: T): [T, React.Dispatch<React.SetStateAction<T>>] => {
  const isMounted = useMounted();
  const [state, setState] = useState(initialValue);

  const newSetState = (value: any) => {
    if (isMounted.current) {
      return setState(value);
    }
  };
  return [state, newSetState];
};

Enter fullscreen mode Exit fullscreen mode

Problems?

  • useStateMounted is not recognized by the eslint-plugin and asking for addition on the dependency array ofuseEffect.
Collapse
 
patrik_masiar profile image
Patrik Mäsiar

Such good practice! I like this in the way that you change state and your component is re-rendered only once. Good job! 👍🏻