You Might not need Redux
Tags: react, redux, webdev, javascript
Published: 2019-05-07 16:45:19



Redux is an awesome state management library. While being very minimalistic, it offers the structure and order that can easily be neglected in a React project. That's why I would automatically install it every time I start a React project. It would be my cache, my application state keeper ~~and best friend~~.

Then I discovered Apollo Client that can manage cache for you without much overhead. That's when Redux' role was starting to _reduce_ (pun intended) in my dev experience. Basically, I would only use it for authentication and network/server state.

And then React released hooks, including the [`useReducer`](https://reactjs.org/docs/hooks-reference.html#usereducer) one, whose usage reminded me a lot of Redux... At this point I started to rethink whether I really need an entire extra dependency in my code to manage very little.

In this post I will describe my reasoning behind moving away from Redux and go through the process of migration. Hopefully it can help some of you keep an open mind about your favourite libraries and realise when it might be time to let go of them :)

## Why leave Redux?

There are a few reasons that pushed me into exploring replacing Redux with hooks API.

First, I was installing an extra NPM package for just 4 actions and 2 states. That seemed very excessive and added complexity to the project. Besides, React offers everything you need for basic app state management. If you don't use it, it's a waste of code.

Second, I was starting to get annoyed with how complex typing connected components can get... I had to write a whole lot of extra code sometimes just to know whether the user is authenticated or not.

Third, like many, I instantly fell in love with React hooks and how well they tie into functional components (my second favourite thing in frontend dev after hooks themselves).

## How did I leave Redux without breaking anything?

My Redux store had 2 reducers combined: `auth` and `appStatus`. First, let's see how I migrated `auth`.

`auth` state was simple:

```typescript
interface AuthState {
  isSignedIn: boolean
  token: string
  user: User
}
```

With it came 2 actions: `signIn` and `signOut`.

First thing that I noticed is that React's `useReducer` hook has the same reducer signature as Redux. So the great thing is that you can totally reuse your reducers! However, I could not just put reducer in a context. I needed to be able to _update it from the nested components_, so I followed the tutorial from the official docs called [Updating Context from a Nested Component](https://reactjs.org/docs/context.html#updating-context-from-a-nested-component) (what a coincidence??). Thus this code was born:

```tsx
// contexts/AuthContext.ts

export const AuthContext = createContext<AuthContextState>({
  isSignedIn: false,
})
export const AuthProvider = AuthContext.Provider
```

```tsx
// components/AuthContextContainer.tsx

import {
  auth,
  signIn as signInAction,
  signOut as SignOutAction,
} from '../reducers/auth.ts'

export const AuthContextContainer: FC = ({ children }) => {
  const [state, dispatch] = useReducer(auth)

  const signIn = useCallback((user: User, token: string) => {
    dispatch(signInAction(user, token))
  }, [])
  const signOut = useCallback(() => {
    dispatch(signOutAction())
  }, [])

  return (
    <AuthProvider value={{ ...state, signOut, signIn }}>
      {children}
    </AuthProvider>
  )
}
```

Bam! There's the Redux auth store. Now to use it in my components instead of `connect`ing them, I simply had to do this:

```tsx
export const SignInContainer: FC = () => {
  const { signIn } = useContext(AuthContext)

  const onSubmit = async ({email, password}: SignInFormValues): void => {
    const { token, user } = await getTokenAndUserFromSomewhere(email, password)
    signIn(user, token)
  }

  return (
    // ... put the form here
  )
}
```

Now I can sign into the app and browse around! What happens though when I reload the page? Well, as you might've already guessed, the app has no idea I was ever signed in, since there's no state persistence at all... To handle that I modified the `AuthContextContainer` to save the state into `localStorage` on every change:

```tsx
export const AuthContextContainer: FC = ({ children }) => {

  // LOOK HERE
  const initialState = localStorage.getItem('authState')

  const [state, dispatch] = useReducer(
    auth,

    // AND HERE
    initialState ? JSON.parse(initialState) : { isSignedIn: false },
  )

  const signIn = useCallback((user: User, token: string) => {
    dispatch(signInAction(user, token))
  }, [])
  const signOut = useCallback(() => {
    dispatch(signOutAction())
  }, [])

  // AND HERE
  useEffect(() => {
    localStorage.setItem('authState', JSON.stringify(state))
  }, [state])

  return (
    <AuthProvider value={{ ...state, signOut, signIn }}>
      {children}
    </AuthProvider>
  )
}
```

Now the `useReducer` hook gets an initial state and it's persisted on every change using the `useEffect` hook! I don't know about you, but I think it's awesome. A component and a context do exactly what an entire library used to do.

Now I'll show you what I did with the `appStatus` state. `appStatus` only had one job: watch for the network availability and store whether we're online or offline. Here's how it did it:

```typescript
export const watchNetworkStatus = () => (dispatch: Dispatch) => {
  window.addEventListener('offline', () =>
    dispatch(networkStatusChanged(false)),
  )
  window.addEventListener('online', () => dispatch(networkStatusChanged(true)))
}

export interface AppStatusState {
  isOnline: boolean
}
const defaultState: AppStatusState = {
  isOnline: navigator.onLine,
}

export const appStatus = (
  state: AppStatusState = defaultState,
  action: AppStatusAction,
): AppStatusState => {
  switch (action.type) {
    case AppStatusActionTypes.NetworkStatusChanged:
      return {
        ...state,
        isOnline: action.payload.isOnline,
      }

    default:
      return state
  }
}
```

You can see that to watch for network status, I was using a thunk, which isn't offered by the `useReducer` hook. So how did I handle that?

First, like before, I needed to create the context:

```typescript
// contexts/AppStatusContext.ts

export const AppStatusContext = createContext({ isOnline: false })
export const AppStatusProvider = AppStatusContext.Provider
```

Then like for auth, I started writing a container that will handle the logic. That's when I realized that I don't even need a reducer for it:

```tsx
// components/AppStatusContainer.tsx

export const AppStatusContainer: FC = ({ children }) => {
  const [isOnline, setIsOnline] = useState(true)

  const setOffline = useCallback(() => {
    setIsOnline(false)
  }, [])
  const setOnline = useCallback(() => {
    setIsOnline(true)
  }, [])

  useEffect(() => {
    window.addEventListener('offline', setOffline)
    window.addEventListener('online', setOnline)

    return () => {
      window.removeEventListener('offline', setOffline)
      window.removeEventListener('online', setOnline)
    }
  })

  return <AppStatusProvider value={{ isOnline }}>{children}</AppStatusProvider>
}
```

Thus I not only got rid of an extra dependency, but also reduced complexity! And that particular thunk could simply be replaced with a `useEffect` hook.

---

That's how in a few short steps (and about an hour) I managed to reduce the size of my app bundle and get rid of some unnecessarily complex logic. The lesson here is that no matter how useful a library can be, it can and will happen that you don't need it. You just gotta keep and open mind about it and notice when it happens :)

I hope my experience will give some of you the courage to try new things and discover new dev experiences for yourselves!

PS: hooks are awesome! If you still haven't, you should totally start using them.