Apr 14, 2023
Philip Jonas
Explore the benefits of global state and discover two methods to implement it: the React context API and the Clerk React context API component.
React global state refers to the data or state that all components in a React application share. This data is typically stored in a global object, such as a state manager like Redux or the React context API, and it can be accessed by any component in the application that needs it.
By using global state, React components can communicate with each other and share data even if they're not directly connected in the component hierarchy. This allows data in a React application to be better organized and managed.
This article starts by explaining the benefits of using global state in React and when it's best to use it. It then shows you how to implement global state in React using two methods: a custom implementation of a React context using the context API and an implementation of the Clerk React context API component. Lastly, it considers when each of these methods is most useful.
The code for the tutorial can be found in this GitHub repository.
As mentioned, using global state in a React application can help to make your code more organized, manageable, and performant.
Global state makes managing shared data easier. Storing all of an application's state in a global object makes it easier to manage from a single location rather than having to pass data down through the component hierarchy. It can make code easier to understand and maintain.
It also enables communication between components that are not directly connected. Any component in an application can access and update the shared data even if it's not directly connected to the component that initially stored the data. This can be useful for triggering updates or changes in other parts of the application.
Lastly, using global state improves performance. Because the global state is stored in a centralized location, components that need the same data can access it from the global state as opposed to each component having to fetch the data separately. This can improve the performance of an application by reducing the amount of data that needs to be fetched and processed.
Using React global state is not a must, but it can be a useful tool in certain situations.
It's most useful when data is needed by multiple components in an application because it ensures that these components all have access to the latest and most up-to-date version of the data. For example, for a login form that's used by multiple components, the global state could be used to store the user's authentication status and other information, which could then be accessed by any component that needs it.
Another common use case for React global state is to allow components to update the data in the global state. For example, a shopping cart application could use the global state to store a list of items in the cart and then allow any component that displays the cart items to update the list when a user adds or removes an item. It allows any component that displays the cart items to always have the latest version of the data.
To follow along with this tutorial, you'll need the following prerequisites installed on your system:
In this section, you'll learn the basic structure for creating a globally managed state object using a context provider to get and update information without creating a dependency chain of properties.
Clone the project from GitHub.
Below is a directory tree of the cloned project. Make sure that you are on the main
branch but running git checkout main
in the project root.
1|..2├── index.html3├── package.json4├── README.md5├── src # Main application source code6| ├── app-context # Application context7| | └── user-context.tsx # Main application user context8| | └── user-context-provider.tsx # Application context provider9| ├── components # UI Components10| | └── AdminPage # Admin page locked by a user object11| | └── index.tsx12| | └── LoginStatus # Login status display component13| | └── index.tsx14| | └── SignInSignOut # Log in and log out buttons15| | └── index.tsx16| | └── WelcomePage # Landing page17| | └── index.tsx18| ├── App.tsx19| ├── assets20| ├── main.tsx21| └── vite-env.d.ts # ViteJs env type declarations22├── tsconfig.json23├── tsconfig.node.json24└── vite.config.ts # ViteJs configuration
Once the project has been cloned, you need to install the project's node modules using npm i
or npm install
. To test the React app, use npm run dev
to start the server at http://localhost:5173.
Once the server is running, the following should appear in the terminal window to indicate that the server is running correctly:
1VITE v4.0.3 ready in 3140 ms23➜ Local: http://localhost:5173/4➜ Network: use --host to expose5➜ press h to show help
If your web browser does not open automatically, open the browser and enter the following URL in the address bar: http://localhost:5173/welcome
. Here is what will be visible in the browser:
Let's have a look how this first example is configured in ~./src/app-context
. This directory has two files. The first is user-context.tsx, which is where the React context state is first declared, then initialized with default values, and lastly exported for global use when the context provider wraps the global application node:
1// FILE - ./src/app-context/user-context.tsx2// ----------------------------------34import React from "react";56export interface UserContract {7id?: number,8username?: string,9firstName?: string,10email?: string11}1213// The dummy user object used for this example14export const DummyUser:UserContract ={15id: 1,16username: "MyUserName",17firstName: "John",18email: "john@doe.com"19}2021/**22* Application state interface23*/24export interface AppState {25user?: UserContract;26updateState: (newState: Partial<AppState>) => void;27}2829/**30* Default application state31*/32const defaultState: AppState = {33user: {},34updateState: (newState?: Partial<AppState>) => {},35};3637/**38* Creating the Application state context for the provider39*/40export const UserContext = React.createContext<AppState>(defaultState);
First, the AppState
interface is declared containing the user
object type and updateState
function. Included on the AppState
interface is the updateState
method, which will accept a partial state object that allows specific sections of the user object to be updated.
After the interface has been declared, the default state is created and set to the type of the AppState
and then defaulted. In this case, it's an empty object.
Finally, the React context is created and exported as UserContext
with the React.createContext SDK API.
Inside ./src/app-context/user-context-provider.tsx is UserContextProvider
. This is where the global state and provider methods like updateState
get assigned.
1// FILE - ./src/app-context/user-context-provider.tsx2// ----------------------------------34import React, { useState } from "react";5import { AppState, UserContext } from "./user-context";67interface Props {8children: React.ReactNode;9}1011/**12* The main context provider13*/14export const UserContextProvider: React.FunctionComponent<Props> = (15props: Props16): JSX.Element => {1718/**19* Using react hooks, set the default state20*/21const [state, setState] = useState({});2223/**24* Declare the update state method that will handle the state values25*/26const updateState = (newState: Partial<AppState>) => {27setState({ ...state, ...newState });28};2930/**31* Context wrapper that will provider the state values to all its children nodes32*/33return (34<UserContext.Provider value={{ ...state, updateState }}>35{props.children}36</UserContext.Provider>37);38};
To make the global state available to the entire application, you need to provide it as the main parent wrapper to all the child nodes. This is done by setting the <App />
component as the child node of UserContextProvider
:
1// FILE - ./src/main.tsx2// ----------------------------------34import React from 'react'5import ReactDOM from 'react-dom/client'6import App from './App'7import { UserContextProvider } from './app-context/user-context-provider'89ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(10<React.StrictMode>11<UserContextProvider> // Context provider wrapper12<App /> // Every child node will have access to the UserContext state.13</UserContextProvider>14</React.StrictMode>,15)
At this stage, the context can be included in the component using the useContext React hook whenever it needs to be used:
1// FILE - ./src/App.tsx2// ----------------------------------34const App: React.FunctionComponent = (): JSX.Element => {5/*6* Using react's useContext to tell the component which context to use,7* the relevant state properties can be extracted. In the example below,8* the user object, clerk object and the updateState method is made available.9*/10const { user, clerk, updateState } = useContext(UserContext);1112/* Application logic ... */1314return (15<Container maxWidth="sm">16<BrowserRouter>17{/* Application components */}18</BrowserRouter>19</Container>20);
Now, let's consider another method that you can use to achieve global state. Clerk offers its own context provider called ClerkProvider. It provides a wrapper for a ReactJS application that handles the user session and state as well as a list of child components and hooks for use in your application.
First, you need to create a Clerk application.
Clerk handles authentication requests when users sign in. For Clerk to know which sign-in session belongs to which client account, there needs to be a unique reference between the React application and the Clerk API. This is achieved by setting up an application on the Clerk platform to get a unique application key against which the sign in will occur.
Now, return to the repository you cloned for the previous example and check out the Clerk branch by running the following commands:
~/$: cd ./clerk-dev-global-state-with-context
~/clerk-dev-global-state-with-context/$: git checkout feat/main-clerk_devi
Here's the file structure for the project once the main-clerk_dev
branch has been checked out:
1.2├── index.html3├── package.json4├── README.md5├── src # Main application source code6| ├── App.tsx # Application main component7| ├── components # Application components8| | ├── AdminPage # Admin page requires login session9| | ├── common # Holds the application routes10| | ├── LoginStatus # Uses ClerkProvider context to show login status11| | └── WelcomePage # Welcome page that incorporates login status component12| ├── main.tsx # Application render component13| └── vite-env.d.ts # ViteJs env type declarations14├── tsconfig.json15├── tsconfig.node.json16└── vite.config.ts # ViteJs configuration
In this section, you'll implement the Clerk context provider as well as the Clerk React components. These components help make it simple to manage content based on login status. Wrapping elements with the node list <SingedIn />
means that they will only show when the user is signed in, and conversely, wrapping them with <SignedOut />
will only display child nodes and components when the user is out.
First, you need to open the main.tsx file and wrap the <App />
component with the <ClerkProvider />
, as shown below:
1// FILE - ./src/main.tsx2// ----------------------------------34import React from "react";5import ReactDOM from "react-dom/client";6import App from "./App";7import { ClerkProvider } from "@clerk/clerk-react";89const clerkApiKey = import.meta.env.VITE_CLERK_API_URI;1011ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(12<React.StrictMode>1314// The clerk provider wrapper for React Applications.15<ClerkProvider frontendApi={clerkApiKey}>16<App />17</ClerkProvider>1819</React.StrictMode>20);
In the code sample above, the VITE_CLERK_API_URI
environment variable is assigned to the clerkApiKey
, which is the key that you got in the section on setting up Clerk. You then add it to the Clerk provider context wrapper (<ClerkProvider frontendApi={clerkApiKey}>
) used by the Clerk React components.
The code block below shows the main.tsx
component, which is where you would have the main entry into your applications via the <App />
component. Using the ClerkProvider React component, wrap your app component with it. This will set the application component as a child element. Next, add the clerkApiKey
to the ClerkProvider component using the
publishableKey` property.
Now with the application component as the child of the Clerk provider component, all the user components will be passed down into each child component.
1// FILE - ./src/main.tsx23import React from "react";4import ReactDOM from "react-dom/client";5import App from "./App";6import { ClerkProvider } from "@clerk/clerk-react";7import { BrowserRouter } from "react-router-dom";89const clerkApiKey = import.meta.env.VITE_CLERK_API_URI;10console.log(clerkApiKey);1112ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(13<React.StrictMode>14<BrowserRouter>15<ClerkProvider publishableKey={clerkApiKey}>16<App />17</ClerkProvider>18</BrowserRouter>19</React.StrictMode>20);
Clerk has numerous components and functions that can be used inside child components of the <ClerkProvider />
context provider component, allowing for simple implementation and secure authentication for React applications.
As long as all the components that utilize the Clerk hooks and components are child nodes of the <ClerkProvider />
, those components and hooks will interact with the user session and state.
You can use the Clerk useUser()
hook to get the current user state in the form of an object { isLoaded, isSignedIn, user }
. The isLoaded
property checks to see if Clerk has finished requesting the user session, isSignedIn
determines if the current user has signed in, and user
contains the user info for the current user session.
1// FILE - ./src/components/LoginStatus/index.tsx23import React from "react";4import { Typography } from "@mui/material";5import {6useUser7} from "@clerk/clerk-react";89export const LoginStatus: React.FunctionComponent = (): JSX.Element => {10const {user} = useUser(); // << Implementation11return (12<React.Fragment>13<Typography variant="body1">14Login status:{" "}1516// Get the user's first name17{user ? `Signed in as ${user.firstName}` : 'You are signed out'}18</Typography>19</React.Fragment>20);21};
The routes file below shows the use of three Clerk react components. SignIn
, SignedIn
, and SignedOut
:
1// FILE - ./src/components/common/routes.tsx23import { Route, Routes } from "react-router-dom";4// ...56export const MainRoutes: React.FunctionComponent = (): JSX.Element => {7//...8<Route9path="/admin"10element={11<React.Fragment>12<SignedIn>13<AdminPage />14</SignedIn>15<SignedOut>16<SignIn afterSignInUrl="/admin" />17</SignedOut>18</React.Fragment>19}20/>21// ...
The Clerk components are placed as children to the ClerkProvider to allow you to determine which content to show based on login status.
When the SignIn
component (<SignIn afterSignInUrl="/admin" />
) is rendered, it displays the Clerk user sign-in modal on the React application. The optional parameter afterSignInUrl
means the application will update the URL of the page once login is successful.
<SignedIn>
and <SignedOut>
act as conditional wrappers. The current sign-in status will determine whether child components will or will not render. So if a component is wrapped with <SignedOut> {... Child Components} </SignedOut>
, those components will only show if the user is signed out. The same applies to the SignedIn
component.
Clerk lets you store and manage user metadata in a central location. This data can include information such as user preferences, settings, and usage statistics. Using Clerk lets you easily access and update this information as needed without constantly having to ask users for it, resulting in a better user experience.
By having all of this information in one place, developers can tailor their app or website to individual users' needs and preferences. This can help increase engagement and retention as users are more likely to use a personalized app or website.
Three types of user metadata can be configured: private, public, and unsafe.
Private metadata is set and configured on the backend. It's not accessible via the ClerkJS frontend API to ensure that no sensitive information is visible on the client side of the application that could potentially compromise personal information. If any private data needs to be accessible from the frontend API, the user to whom the data belongs must give consent.
Public metadata is also set on the backend but is visible in the ClerkJS frontend API as read-only properties for use on the client side. None of these values can be changed directly from the client side though. The only way to change these values is to update the metadata using the users.updateUsers
method.
Unsafe metadata is set by both the frontend and backend APIs. For example, when a user signs up via the frontend API, custom fields can be set and saved to the user object on the backend, and the same can be done the other way around. These attributes and values can also be set after the user has signed in to the application and then be persisted on the user object with users.updateUsers
.
See a code sample of this below:
12// Source: https://clerk.com/docs/users/user-metadata#unsafe-metadata34import { useUser } from '@clerk/nextjs';56const updateMetadata = async () => {7const { user } = useUser();8const data = 'Data to store unsafely';910try {11const response = await user.update({12unsafeMetadata: { data }13});14if (response) {15console.log('res', response)16}17} catch (err) {18console.error('error', err)19}20};
You've seen how to use both methods to handle global state in React, so how do they compare?
Clerk's state management solution is more suited for larger, more complex applications that need a more powerful and flexible state management solution, while the React Context API is better for smaller applications that don't need as much state management functionality.
So, use the React Context API if:
In contrast, use Clerk if:
Are you interested in trying Clerk? You can sign up for a free tier for POCs as well as individual and private use, or you could look into the paid solutions for large-scale client bases and enterprise-level tools.
Start completely free for up to 10,000 monthly active users and up to 100 monthly active orgs. No credit card required.
Learn more about our transparent per-user costs to estimate how much your company could save by implementing Clerk.
The latest news and updates from Clerk, sent to your inbox.