One of the fundamental aspects when developing a website, an application or simply a program, is the use of components that are as reusable as possible, as the DRY (Don't repeat yourself!) rule explains.
When developing a web app, especially if it is very complex, it is very important to follow this approach, in order to be able to maintain all the components and functions in a much simpler way.
In this article, we're going to see how React Context can help us in sharing values in all the children of the context and how to create custom and more complex ones (with hooks, reducers, memoization). In addition, we will also add strong TypesScript support.
Summary
- Create the project
- Add types
- Create the custom provider
- Create the custom hook
- Implement the provider
- Handle the logic
- Dispatch the values
- Epilogue
Create the project
First, let's create the project, through CRA:
npx create-react-app example --template typescript
And then in /src/contexts (create if doesn't exists) we create userContext.tsx
:
import React, { useContext, createContext, useMemo, useReducer } from "react";
const UserContext = createContext();
export default UserContext;
Add types
Next, we add the types of both the context and the reducers:
interface ContextInterface {
id?: string;
}
interface ActionInterface {
type: setUser
payload: ContextInterface;
}
type DispatchInterface = (action: ActionInterface) => void;
And then we add these interfaces to UserContext:
const UserContext = createContext<
| {
state: ContextInterface;
dispatch: DispatchInterface;
}
| undefined
>(undefined);
We give it an initial value of undefined
, so that later, when we create the provider, we'll pass the reducer to it.
Create the custom provider
But first, we're going to create the reducer:
const reducerUser = (
state: ContextInterface,
action: ActionInterface
): ContextInterface => {
switch (action.type) {
case "setUser":
return { ...state, id: action.payload.id };
default:
throw new Error("Invalid action type in context.");
}
};
Let's now create the custom provider of the userContext
and also declare the reducer, which we will pass as a value to the provider:
const UserProvider: React.FC = ({ children }) => {
const [state, dispatch] = useReducer(reducerUser, {});
const memoizedUser = useMemo(() => ({ state, dispatch }), [state, dispatch]);
return (
<UserContext.Provider value={memoizedUser}>{children}</UserContext.Provider>.
);
};
In case our context is very complex and the value needs to be updated often, I suggest to use useMemo, so React won't do any re-rendering in case the value is equal to the previous one.
In case the context is very simple (like in this case), it's not essential to do this, on the contrary, using useMemo when you don't need it, leads to lower performance. It is shown here as an example only.
Create the custom hook
Now, let's create our custom hook that will allow us to fetch the id of the user from the children of the context.
const useUser = () => {
const user = useContext(UserContext);
return user;
};
So, user, will contain state and dispatch, with which we're going to display and update the user id.
And finally, we export everything:
export { UserProvider, useUser };
Implement the provider
Let's move to App.tsx
and implement what we just created. Let's wrap everything inside our context:
import React from react;
import { Dashboard, UserProvider } from "./index.d";
const App: React.FC = () => {
return (
<UserProvider>
<Dashboard />
</UserProvider>
);
};
export default App;
Handle the logic
In Dashboard.tsx
, we will import the useUser
hook created earlier and with that we will check the id. If it isn't undefined, then, it will show the login.
Otherwise, it will show a simple dashboard that shows the user the user id:
import React from react;
import { useUser, Login } from "../index.d";
const Dashboard: React.FC = () => {
const userContext = useUser();
if (!userContext!.state.id) return <Login />;
return (
<div>
<h2>Dashboard</h2>>
<p>
User logged with <em>id</em>: <strong>{userContext!.state.id}</strong>
</p>
</div>
);
};
As soon as we open the page, the id will obviously be undefined
, because no one logged in.
So, we'll be shown the login page (in Login.tsx
):
import React, { useState } from react;
import { useUser } from "../index.d";
const Login: React.FC = () => {
const [username, setUsername] = useState<string>("");
const [password, setPassword] = useState<string>("");
const handleLogin = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
loginTheUser().then((id) => {});
};
return (
<div>
<div>
<h1>Login</h1>.
<form onSubmit={handleLogin}>
<div>
<input
id="user"
type="text"
value={username}
placeholder="Username"
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div>
<input
type="password"
id="password"
value={password}
placeholder="Password"
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<button type="submit">sign in</button>
</form>
</div>
</div>
);
};
export default Login;
Dispatch the values
To make the context work, however, you must import the custom hook:
const handleUserContext = useUser();
And finally, we add the dispatch call that updates our state:
const handleLogin = () => {
loginTheUser().then((id) =>
handleUserContext!.dispatch({ type: "setUser", payload: { id: id } })
);
};
Ok, now, after logging in, the message we wrote will appear.
It seems to be working, perfect! But what if you want to pass it between multiple components? Do you have to pass it as a prop in the children?
No, otherwise the point of Context would be lost. To display or update the id, just call the hook from a UserContext child and use the state and dispatch variables to update it.
Simple, isn't it?
Epilogue
Now, before we wrap it up, we can install styled-components and add some simple (and ugly) CSS to our project and, to see it finished, I refer you to the repo on Github.
This here is just a basic example, but it can come in very handy when developing complex web apps, where there are some data that need to be passed in all children (such as authentication, or global settings, like dark mode).
Thanks for reading this article! If you encountered any errors or if you want to add something, leave a comment!
Top comments (1)
This was awesome, thank you for doing this.