loading...
Cover image for Replacing Redux with React Contexts

Replacing Redux with React Contexts

rsa profile image Ranieri Althoff Updated on ・2 min read

In my current project, we used to use Redux for things like user authentication, language preferences, viewport width, and in general sharing state between components deep down the tree.

Long ago, we started replacing the shared state with contexts, as it is easier to provide and manage state localized to just a part of the application. That way, the state does not leak upwards, that is, the login page does not have to have access to the to-do list.

A practical example, only relevant bits:

type SetLanguageAction = {
    type: 'SET_LANGUAGE'
    language: string
}

const language = (
    state: string = initialLanguage,
    action: SetLanguageAction
) => {
    if (action.type !== 'SET_LANGUAGE') {
        return state
    }

    localStorage.set('language', action.language)
    return action.language
}

// plus boilerplate to attach it to the store

With context, it becomes:

import React from 'react'

const Context = React.createContext({} as {
    language: string
    setLanguage: React.Dispatch<React.SetStateAction<string>>
})

const LanguageProvider: React.FC = ({ children }) => {
    const [language, setLanguage] = useLocalStorage('language', initialLanguage)

    return (
        <Context.Provider value={{ language, setLanguage }}>
            {children}
        </Context.Provider>
    )
}

const useLanguage = () => React.useContext(Context)
// and that's it!

See, the entire behavior is contained in a single file, and not spread across as is common with Redux (you would have actions.ts, reducers.ts to glue everything).

Additionally, you get full React hooks power, as Providers are React components. As an example, I got access to useLocalStorage (that's from react-use) and don't need to handle local storage by hand.

It helps isolate behavior, but it also helps with stricter typing. In user authentication, if the user state was inside the global state, it's type would be User | null, as the user data is initialized after data is loaded from the backend.

With a localized context, it can be User and we never have to check for nullability or stick ! after accessing the store, as I can suspend rendering while waiting for data to load (say if (!user) return null). It goes really well with SWR :)

Cover image by Timothy Meinberg (see in Unsplash).

Discussion

pic
Editor guide
Collapse
arashkiani profile image
Arash

That's true my bad, although imo if it's done right having a single source of truth have some values.

Collapse
rsa profile image
Ranieri Althoff Author

Yes, nothing stops you from having Redux, but you can also have a top-level context. That's how we do user authentication, actually :) an <AuthProvider> wrapping the application.

Collapse
marksyzm profile image
Mark Elphinstone-Hoadley

We have been using this pattern in a recent project and found it rather tricky to unit test - have you got a simple formula for that?

Collapse
rsa profile image
Ranieri Althoff Author

We don't unit test it specifically but testing-library got you covered

Collapse
arashkiani profile image
Arash

Why would you want to remove redux with context ?

Collapse
marksyzm profile image
Mark Elphinstone-Hoadley

He explains that in the article