Adios Redux: using React hooks and Context effectively
Tags: react, hooks, context, redux
Published: 2020-04-26 06:16:49

![Ciao%20Redux/undraw_active_options_8je6.png](https://dev-to-uploads.s3.amazonaws.com/i/eyr5vt3jo1t3l09ku46p.png)

It's 2020 and React is still the most popular frontend framework in the world. It's not just because it's relatively simpler. The fact that it keeps getting better is what has keeping me hooked (unintentional pun). The introduction of hooks changed the ecosystem from class based components to functions and made writing React way more fun. But there hasn't been a particular state management tool that is the go to option in React.

Redux is really popular. But a major source of complaint with Redux is how difficult it is learn at the beginning due to a lot of boilerplate. Recently I got to see some tweets

{% twitter 1252364934014316544 %}

This led me to go an learning spree and I got to know some exciting patterns and packages which might completely change how you view hooks and global state in general(it did for me).

When I first thought I would write this article series I had way too many options for a title. There was **State Management 2020**, **Custom Hooks in React,** and a few others. But finally I decided to go with Ciao Redux(Goodbye Redux), since that seemed like the end goal for this article series.

This article is inspired by this great talk from Tanner Linsley at JSConf Hawaii 2020. I recommend you to watch it if you haven't already.

{% youtube J-g9ZJha8FE %}

So let's get started.

## How do you see ***State*** in React?

One would simply say, State is all the data present in frontend or it's what you fetch from the server.  But when you have used React for building applications for a few time now, you would understand the point I'm going to make.

{% twitter 1252823896945909770 %}

State can be majorly divided into 2 types:

- UI State
- Server Cache

You maybe wondering WTH I'm talking about. Let me explain.

***UI State*** is the state or information for managing your UI. For example, Dark/Light theme, toggle a dropdown, manage some error state in forms. ***Server Cache*** is the data you receive from the server like a user details, list of products etc.

![Ciao%20Redux/state.png](https://dev-to-uploads.s3.amazonaws.com/i/huulro8rioz01ghd1pfa.png)

## Managing State

Lets start with basics. And build something for example's sake while we are at it. No, not a todo list. We have enough tutorials for that already. We are gonna build a simple application with a login screen and a home screen.

### useState

The `useState` hook allows us to use state inside a functional component. So bye bye all the hassles of declaring state in constructor, accessing it through `this`. One can simply do

```jsx
import { useState } from 'react'

const [name, setName] = useState("")
```

and we get `name` variable and a function to update the variable as `setName`.

Now let's use this knowledge to make a login form for our page.

```jsx
import React, { useState } from 'react'

export default function Login() {
  const [email, setEmail] = useState("")
  const [password, setPassword] = useState("")
  const [emailError, setEmailError] = useState(false)
  const [passwordError, setPasswordError] = useState(false)
	const [isLoading, setIsLoading] = useState(false)

	async function handleSubmit() {
		setIsLoading(true)
		const res = await axios.post(url, {email, password})
		if(res.data.status === "EMAIL_ERROR") {
      setEmailError(true)
    }
    if(res.data.status === "PASSWORD_ERROR") {
      setPasswordError(true)
    }
    // Otherwise do stuff
	}
	return (
		<div>
			<input 
				type="text"
				value={email} 
				onChange={
					e => setEmail(e.target.value)
				} 
			/>
			{emailError && <ErrorComponent type="EMAIL_ERROR" />}
			<input 
				type="password" 
				value={password}
				onChange={
					e => setPassword(e.target.value)
				} 
			/>
			{passwordError && <ErrorComponent type="PASSWORD_ERROR" />}
			{ isLoading
			? <button onClick={() => handleSubmit()}>Sign in</button>
			: <LoadingButton /> }
		</div>
	)
}
```

This works. But this must not be the best way is it. And this can pretty easily go out of hand with addition of few other factors or validation checks for example.

### useReducer

People familiar with Redux must know `useReducer` works just like Redux does. For those who don't  here's how it works.

```
Action -------> Dispatch -------> Reducer --------> Store
```

You create an action and dispatch it which goes through the reducer and updates the store. Let's implement it in the previous example and see how it works.

```jsx
import React, { useReducer } from 'react'

const initialState = {
  user: {
    email: "",
    password: ""
  },
  errors: {
    email: false,
    password: false
  },
	isLoading: false
}

const reducer = (state, action) => {
  switch (action.type) {
    case 'CHANGE_VALUE':
      return {
        ...state,
        user: {
          ...state.user,
          [action.field]: action.data
        }
      }
    case 'ERROR':
      return {
        ...state,
        errors: {
          ...state.errors,
          [action.type]: true
        }
      }
    case 'LOADING':
      return {
	...state,
	isLoading: true
      }
    default:
      return state
  }
} 

export default function Login() {
  const [state, dispatch] = useReducer(reducer, initialState)
  
  async function handleSubmit() {
		dispatch({type: 'LOADING'})
		const res = await axios.post(url, store.user)
		if(res.data.status === "EMAIL_ERROR") {
      dispatch({type: 'ERROR', field: "email"})
    }
    if(res.data.status === "PASSWORD_ERROR") {
      dispatch({type: 'ERROR', field: "password"})
    }
    // Otherwise do stuff
	}

	return (
		<div>
			<input 
				type="text"
                value={state.user.email} 
				onChange={
					e => dispatch({type: "CHANGE_VALUE", data: e.target.value, field: "email"})
				} 
			/>
			{state.errors.email && <ErrorComponent type="EMAIL_ERROR" />}
			<input 
				type="password" 
				onChange={
                                        value={state.user.password}
					e => dispatch({type: "CHANGE_VALUE", data: e.target.value, field: "password"})
				} 
			/>
			{state.errors.password && <ErrorComponent type="PASSWORD_ERROR" />}
			<button onClick={() => handleSubmit()}>Sign in</button>
		</div>
	)
}
```

This looks good, we don't deal with separate functions, we declare one reducer and define some actions and corresponding store changes. This is quite helpful because while using `useState` , we can easily lose track of the number of variables as our requirement grows. You must have a noticed this is much longer than the previous code, which takes us to the next section.

## Abstracting logic from UI

While developing an application in react you should always try to keep your business logic away from your UI code. The UI component, which interacts with the user should only know what interactions the user can do(actions). Plus this provides proper structure as well good maintainability to your codebase. This was well supported by redux in which we can define our actions elsewhere which would take care of all the logic, keeping our UI code clean. But how do we achieve that with hooks. Custom hooks to the rescue!

## Custom Hooks

React allows you to create your own custom hooks for better separation and sharing of logic across components. For the above example, we can create a file called `hooks/useLoginReducer.js`

```jsx
import { useReducer } from 'react'

const initialState = {
  user: {
    email: "",
    password: ""
  },
  errors: {
    email: false,
    password: false
  },
	isLoading: false
}

const reducer = (state, action) => {
  switch (action.type) {
    case 'CHANGE_VALUE':
      return {
        ...state,
        user: {
          ...state.user,
          [action.field]: action.data
        }
      }
    case 'ERROR':
      return {
        ...state,
        errors: {
          ...state.errors,
          [action.type]: true
        }
      }
    case 'LOADING':
      return {
	...state,
	isLoading: true
      }
    default:
      return state
  }
} 

export default function useLoginReducer() {
  const [store, dispatch] = useReducer(reducer, initialState)
  return [store, dispatch]
}
```

Then in the Login component

```jsx
import React from 'react'
import useLoginReducer from '../hooks/useLoginReducer'

export default function Login() {
  const [store, dispatch] = useLoginReducer()
	...
}
```

Voila! We separated the logic from the component and it looks so much cleaner now. Custom hooks can be used as such to a great effect for separation of concerns.

Let's go ahead to the best part. 

## Global State

Managing global state is what 3rd party libraries like Redux aim to provide, because prop drilling is hell. React has Context API, which allows to pass data between components. Context allows you declare a `Provider` which stores or initialises the data and `Consumer` which can read or update the data. It is used by Redux in the background, but 

- it was unstable for a lot of time
- needed render props which led to less readability

With the introduction of React hooks however, using context became a lot more easier. One can easily declare a global state and use them by combining `hooks` and `context`. Let's take a look at an example we used above. Suppose after login you want update the global store with user's details which can be used in a Navbar component to display the user's name.

We declare a context first and use hooks to store and update data.

```jsx
const globalContext = React.createContext()

const intialState = {
	user: {
		...
	}
}

const reducer = {
	...
}

export const StoreProvider = ({children}) => {
  const [store, dispatch] = React.useReducer(reducer, initialState)
	
	//memoizes the contextValue so only rerenders if store or dispatch change
	const contextValue = React.useMemo(
		() => [store, dispatch],
		[store, dispatch]
	)

  return (
    <globalContext.Provider value={contextValue}>
      {children}
    </globalContext.Provider>
  )
}

export function useStore() {
  return React.useContext(globalContext)
}
```

So let me explain through the code here. We first create a context. Then we are using useReducer inside a component to create the store and dispatch method. We are using `useMemo` to create a context variable to update only when one of it's dependencies change. Then we are returning the `context.Provider` component with value as the context variable. In the last part we are using the `useContext` hook which simply allows us to use the context inside a functional component provided it lies inside the `Provider`.

```jsx
// App.js
import React from 'react';
import { StoreProvider, useStore } from './context';

function App() {
  return (
    <StoreProvider>
      <Navbar />
      ...
    </StoreProvider>
  );
}

// Login.js
import React from 'react';
import { useStore } from './context'

function Login() {
    const [, dispatch] = useStore()
    ...
    function handleSubmit() {
        ...
        dispatch(...)
    }
}

// Navbar.js
import React from 'react';
import { useStore } from './context';

function Navbar() {
	const [{user}, dispatch] = useStore()
	return (
		...
		<li>{user.name}</li>
	)
}
```

So we wrap the app component in the `StoreProvider` and use the `useStore` function we returned to access the store value and dispatch function at a nested component. Sounds awesome right. Umm not so much. There are a lot of issues in this. Let's take a look.

- Firstly, since we are exporting both `store` and `dispatch`. Any component which updates the component (uses dispatch only) and doesn't use the store will also rerender everytime the state changes. This is because a new data object is formed everytime context value changes. This is undesirable.
- Secondly, we are using a single store for all our components. When we would add any other state to the reducer initialState, things will grow a lot. Plus every component that consumes the context will rerender everytime the state changes. This is undesirable and can break your application.

So what can we do to solve these. A few days I came across this tweet thread

{% twitter 1252609786635587589 %}

{% twitter 1252371134365888515 %}

Problem solved. This is what we needed. Now's let's implement that and I'll explain it along with.

For the first problem, we can simply separate the store and dispatch into to different contexts `DispatchContext` for updating the store and `StoreContext` for using the store. 

```jsx
const storeContext = React.createContext()
const dispatchContext = React.createContext()

const intialState = {
	user: {
		...
	}
}

const reducer = {
	...
}

export const StoreProvider = ({children}) => {
  const [store, dispatch] = React.useReducer(reducer, initialState)

  return (
    <dispatchContext.Provider value={dispatch}>
      <storeContext.Provider value={store}>
	    {children}
      </storeContext.Provider>
    </dispatchContext.Provider>
  )
}

export function useStore() {
  return React.useContext(storeContext)
}

export function useDispatch() {
	return React.useContext(dispatchContext)
}
```

Then simply we can only import `useDispatch` or `useStore` according to our case.

```jsx
// App.js
import React from 'react';
import { StoreProvider } from './context';

function App() {
  return (
    <StoreProvider>
      <Navbar />
      ...
    </StoreProvider>
  );
}

//Login.js
import React from 'react';
import { useDispatch } from './context'

function Login() {
    const dispatch = useDispatch()
    ...
    function handleSubmit() {
        ...
        dispatch(...)
    }
}

// Navbar.js
import React from 'react';
import { useStore } from './context'

function Navbar() {
	const {user} = useStore()
	return (
		...
		<li>{user.name}</li>
	)
}
```

Now moving on to the second problem. It's really simple, we don't need to create a single store. I had difficulty using context previously primarily due to this reason only. Even in Redux, we separate reducers and combine them. 

We can simply define a function which takes in `initialState` and `reducer` and returns a store. Let's see how it's done.

```jsx
import React from 'react'

export default function makeStore(reducer, initialState) {
  const storeContext = React.createContext()
  const dispatchContext = React.createContext()
  
  const StoreProvider = ({children}) => {
    const [store, dispatch] = React.useReducer(reducer, initialState)
  
    return (
      <dispatchContext.Provider value={dispatch}>
        <storeContext.Provider value={store}>
          {children}
        </storeContext.Provider>
      </dispatchContext.Provider>
    )
  }
  
  function useStore() {
    return React.useContext(storeContext)
  }
  
  function useDispatch() {
    return React.useContext(dispatchContext)
  }

  return [StoreProvider, useStore, useDispatch]
}
```

Then we can declare our `userContext` as follows.

```jsx
import makeStore from '../store'

const initalState = {
  user: {
		...
	}
}

const reducer = (state, action) => {
  switch (action.type) {
    ...
		...
  }
}

const [
  UserProvider,
  useUserStore,
  useUserDispatch
] = makeStore(reducer, initalState)

export { UserProvider, useUserStore, useUserDispatch }
```

And finally use it when we need

```jsx
// App.js
import React from 'react';
import { UserProvider } from './userStoreContext';

function App() {
  return (
    <UserProvider>
      <Navbar />
      ...
    </UserProvider>
  );
}

// Login.js
import React from 'react';
import { useUserDispatch } from './userStoreContext'

function Login() {
    const dispatch = useUserDispatch()
    ...
    function handleSubmit() {
        ...
        dispatch(...)
    }
}

// Navbar.js
import React from 'react';
import { useUserStore } from './userStoreContext'

function Navbar() {
  const {user} = useUserStore()
  return (
    ...
    <li>{user.name}</li>
  )
}
```

Done. If we want another store we can simply make another store and wrap it around our app or the components where you want to use it. For example

```jsx
function App() {
  return (
    <UserProvider>
        <Navbar />
        <ProductProvider>
            <Products />
        </ProductProvider>
    </UserProvider>
  );
}
```

Whooh. This was it for the first part of the series. Hope you have learned how to use hooks and context effectively. In the next articles I'm going to talk about `react-query`and how to deal with server cache. Stay tuned.

## Further Reading

- [https://reactjs.org/docs/hooks-custom.html](https://reactjs.org/docs/hooks-custom.html)
- [https://reactjs.org/docs/hooks-reference.html#usereducer](https://reactjs.org/docs/hooks-reference.html#usereducer)
- [https://reactjs.org/docs/context.html#consuming-multiple-contexts](https://reactjs.org/docs/context.html#consuming-multiple-contexts)
- [https://reactjs.org/docs/hooks-reference.html#usecontext](https://reactjs.org/docs/hooks-reference.html#usecontext)
- [https://kentcdodds.com/blog/how-to-use-react-context-effectively](https://kentcdodds.com/blog/how-to-use-react-context-effectively)