DEV Community

Cover image for Replacing Redux with React Contexts
Ranieri Althoff
Ranieri Althoff

Posted on • Edited on

Replacing Redux with React Contexts

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
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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).

Top comments (6)

Collapse
 
marksyzm profile image
Mark Elphinstone-Hoadley

He explains that in the article

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

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

Collapse
 
rsa profile image
Ranieri Althoff • Edited

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.