Initially, Reacts global and components level state managements was once the most time-consuming part whenever I start a new project, It's always the "which one to use?" question.
Should I use redux or redux-saga or mobx-react or mobx-state-tree or several other packages, but haven't gone back and forth Reacts own Context API combined with Reducer has been the real winner for me.
No external package or no need to state learning any package APIs.
So let me share what my store looks like, both in ReactNative and Web.
// ~/lib/store.tsx
import { createContext, Dispatch } from "react";
export interface IState {
user: User;
notifications: Array<INotification>;
}
export type Actions = {
kind: keyof IState;
payload?: any;
};
export function appReducer(state: IState, action: Actions) {
state = { ...state, [action.kind]: action.payload };
return state;
}
interface IContextProps {
state: IState;
dispatch: Dispatch<Actions>;
}
export const AppContext = createContext({} as IContextProps);
export const AppStateProvider = ({ children }) => {
const [state, dispatch] = useReducer(appReducer, {
user: null,
notifications: [],
});
return <AppContext.Provider value={{ state, dispatch }}>{children}</AppContext.Provider>;
};
Then register the AppStateProvider at your app root
// ~/pages/_app.tsx
import { StateProvider } from "~/lib/store";
export default function App({ Component, pageProps }) {
return (
<AppStateProvider>
<Component {...pageProps} />
</AppStateProvider>
);
}
Then usage will look like this
// ~/pages/index.tsx
import React, { useContext, useState } from "react";
export default function HomePage() {
const { state, dispatch } = useContext(AppContext);
const [form, setForm] = useState({ name: "", email: "" });
function onChange(ev: React.FormEvent<{}>) {
const target = ev.target as HTMLInputElement;
const value = target.type === "checkbox" ? target.checked : target.value;
const name = target.name;
setForm((v) => ({ ...v, [name]: value }));
}
function login(ev: React.FormEvent<HTMLFormElement>) {
ev.preventDefault();
if (Object.values(form).every(Boolean)) {
dispatch({ kind: "user", payload: form });
} else {
alert("Please fill the form well");
}
}
return (
<div>
{state.user ? (
<div>
<hi>Welcome {state.user?.name}</hi>
</div>
) : (
<div>
<h1>Login</h1>
<form action="" method="post" onSubmit={login}>
<section>
<label htmlFor="name">Name</label>
<div>
<input name="name" id="name" value={form.name} onChange={onChange} />
</div>
</section>
<section>
<label htmlFor="email">Email</label>
<div>
<input name="email" id="email" value={form.email} onChange={onChange} />
</div>
</section>
<button>Login</button>
</form>
</div>
)}
</div>
);
}
And also know that the dispatch state key kind
is well-typed
The end!.
I hope this helps.
Top comments (7)
While a great exercise, this is horrible for performance unfortunately and will not scale.
One of the hardest parts about React state management is managing re-renders.
A component in well written redux (or mobx or whatever, for that matter) app will only re-render if the parts of state it uses have changed. In redux this is achieved by writing good selectors, in mobx it is the automatic proxy that does that, which is the main selling point of the library.
In your example every component that uses
AppContext
will re-render each time every single thing changes. This is not scalable, and that's where libraries still come into play.While what you said might be totally correct, you should not put a component level state in your global state
Whatever state field I have in my global store I am expecting it to affect my entire app so I don't think that's much of a big deal
That is true if you use it wrong, but if you use context correctly it works fine at large scale and I would anyway never put server state in Redux that's why I use for server data ReactQuery and for client state context + useReducer, Redux is horrible when used for server data.
I like to export a hook directly for usage of the store in your case add in store.tsx 'export const useAppState = () => useContext(AppContext);
You can now use it everywhere and it saves you imports in your components.
That makes sense thanks for the tips
Exactly which why I wrote the post to help others because I noticed a lot of people are still stuck with redux and others
I understand your position but frankly, I'm on a project that implemented a solution similar to yours, and now that it's grown and become a bigger thing, the in-house solution starts to show its flaws...
As another commented, re-renders became problematic so a memoization layer had to be implemented, and now, as we want to use other stuff like device persisting, we must reinvent the wheel... that's why we plan on refactoring the whole thing to use a library...
Honestly, have a look at Redux Toolkit and you'll see that the most annoying stuff about Redux becomes way easier! 😊