React Service Provider: Applications and Best Practices

The React Provider Pattern is one of the most popular emerging React design patterns in many modern React apps, with variations promoted by React professionals across the board.

This provider pattern is largely inspired by and widely used in the open-source repository google/web-stories-wp. Many examples will refer to this pretty large codebase, which contains over 20 occurrences of this pattern throughout the program.

This page discusses the beginnings of React Service Provider, explores its applications, and provides extra information on how to best use it.

React Service Provider Example

Let’s take a very simple example with a User Context that contains the name of the presently active user:

import { createContext  } from ‘react’;

const UserContext = createContext({});

function Name() {

return (

<UserContext.Consumer>

{({ name }) => (

<p>Hello, {name}</p>

)}

</UserContext.Consumer>

);

}

 

function App() {

const name = ‘World’;

const value = { name };

 

return (

<UserContext.Provider value={value}>

<h1>Welcome</h1>

<Name />

</UserContext.Provider>

)

}

This is broken down into three primary parts:

  • createContext: Create a new context object that can be provided and consumed, with a default value if no parent provider is found.
  • SomeContext.Provider: A React component that adds a new value to any consumer below it in the component tree.
  • SomeContext.Consumer: A React component that expects a render prop that renders the value of the nearest provider in the component tree above.

This appears to be a pretty simple structure, but it is really powerful and enables some very complicated creations, particularly when combined with hooks, as we will see in the following section.

It is important to note that a consumer may have no or numerous suppliers above them.

However, both of these are rather uncommon cases, which we will ignore for the time being. In the next sections, we’ll always match a customer to a single parent supplier. However, each parent supplier often serves a large number of consumers.

Shortcomings of the React Context Consumer

The consumer component stated in the preceding section is a little strange to operate, as is common when utilizing render properties. You cannot directly access the values in your component; instead, you must place your logic within the render prop method or send the values to a child component.

// This will not work

function BrokenWelcome() {

const message = `Hello, ${name}`;

return (

<UserContext.Consumer>

{({ name }) => (

<p>{message}</p>

)}

</UserContext.Consumer>

);

}

 

// Moving the message inside the render prop will work:

function NestedWelcome() {

return (

<UserContext.Consumer>

{({ name }) => {

const message = `Hello, ${name}`;

return (

<p>{message}</p>

);

}}

</UserContext.Consumer>

);

}

 

 

// Moving the message inside the render prop will work:

 

function WelcomeMessage({ name }) {

const message = `Hello, ${name}`;

return (

<p>{message}</p>

);

}

function ParentWelcome() {

return (

<UserContext.Consumer>

{({ name }) => (

<WelcomeMessage name={name}>

)}

</UserContext.Consumer>

);

}

While the latter two solutions are certainly workable, they are not as clean as you might prefer. The useContext hook comes to the rescue!

React’s useContext hook

React has provided us with the very useful hook useContext, which allows us to more easily retrieve the current value of the nearest provider for a particular context. It is a hook that returns the same value as when a context consumer’s render prop is invoked and is most commonly disassembled as such.

The above example would now just say:

function BestWelcome() {

const { name } = useContext(UserContext);

const message = `Hello, ${name}`;

return (

<p>{message}</p>

);

}

Exploring the React Services Example

Here is the practical react service example:

jsx

import React, { createContext, useState, useContext } from ‘react’;

// Create the AuthContext

const AuthContext = createContext();

 

// Create the AuthServiceProvider component

const AuthServiceProvider = ({ children }) => {

const [user, setUser] = useState(null);

 

const login = (username, password) => {

// Perform login logic (e.g., API call)

// For demonstration, we’ll just set a dummy user

setUser({ username });

};

 

const logout = () => {

// Perform logout logic

setUser(null);

};

 

return (

<AuthContext.Provider value={{ user, login, logout }}>

{children}

</AuthContext.Provider>

);

};

 

// Custom hook to use the AuthContext

const useAuth = () => {

return useContext(AuthContext);

};

 

// Example usage in a component

const LoginButton = () => {

const { login } = useAuth();

 

return <button onClick={() => login(‘user’, ‘password’)}>Login</button>;

};

 

const LogoutButton = () => {

const { logout } = useAuth();

 

return <button onClick={logout}>Logout</button>;

};

 

const UserProfile = () => {

const { user } = useAuth();

 

if (!user) {

return <div>Please log in</div>;

}

 

return <div>Welcome, {user.username}</div>;

};

 

// Wrapping the application with AuthServiceProvider

const App = () => {

return (

<AuthServiceProvider>

<UserProfile />

<LoginButton />

<LogoutButton />

</AuthServiceProvider>

);

};

 

export default App;

Putting it all together: The React Provider Pattern

Going from the above to what I perceive to be the React Provider Pattern is simply a matter of reorganizing all of the elements into a more organized framework.

This structure always consists of the same three pieces (4 if you count an index file) placed within its own package:

  • folder/context.js – the file defining the context variable used by the structure
  • folder/provider.js – the (main) context provider, wrapping whatever children it’s given with a dynamic context
  • folder/use.js – a custom hook giving components access to the current context value

The final criterion is to use a systematic approach to context value in order to improve familiarity and reuse.

Many patterns are available here, but a very simple structure has proved to serve practically all important applications thus far, with very few exceptions.

const value = {

state: {

// Here are values, that are to be considered immutable

// Immer.js can be used to ensure immutability is obeyed

},

actions: {

// Here goes function that manipulate the above values

// or return values based on the context internals

},

};

Putting all this together, we get this codebase for our very simple UserProvider:

// File: user/context.js

import { createContext } from ‘react’;

const UserContext = createContext({state: {}, actions: {}});

// File: user/use.

import { useContext } from ‘react’;

import UserContext from ‘./context’;

function useUser() {

return useContext(UserContext);

}

 

// File: user/provider.js

import { useState } from ‘react’;

function UserProvider({ children }) {

const [name, setName] = useState(‘World’);

const value = {

state: { name },

actions: { setName },

};

return (

<UserContext.Provider value={value}>

{children}

</UserContext.Provider>

)

}

And if we want to use it in our app, we will do it like this (with the same functionality as before):

// File: app.js

import UserProvider from ‘./user/provider’;

import ShowName from ‘./show’;

import EditName from ‘./edit’;

function App() {

return (

<UserProvider>

<h1>Welcome</h1>

<ShowName />

<EditName />

</UserProvider>

)

}

 

// File: show.js

import useUser from ‘./user/use’;

function ShowName() {

const { state: { name } } = useUser();

return <p>Hello, {name}</p>;

}

 

// File: edit.js

import useUser from ‘./user/use’;

function EditName() {

const { actions: { setName } } = useUser();

const handleClick = () => setName(prompt(‘What is your name?’));

return (

<button onClick={handleClick} />

);

}

Again, we can see this in action in this live demonstration. On the surface, the example is identical to the previous one, but the context is now internalized between the provider and the custom hook, resulting in a much cleaner interface between the provider and the rest of the application.

React services folder structure

The first stage adheres to the guideline: One file to rule them all. Most React projects begin with a src/ folder and a single src/App.js file containing an App component. At least that’s what happens when you use create-react-app. It’s a function component that just renders JSX:
Import * as React from’react’;

const App = () => {

const title = ‘React’;

 

return (

<div>

<h1>Hello {title}</h1>

</div>

);

}

 

export default App;

This component will eventually add additional functionality, expand in size, and need to be split into standalone React components. We are extracting a React list component with another child component from the App component.

import * as React from ‘react’;

const list = [

{

id: ‘a’,

firstname: ‘Robin’,

lastname: ‘Wieruch’,

year: 1988,

},

{

id: ‘b’,

firstname: ‘Dave’,

lastname: ‘Davidds’,

year: 1990,

},

];

 

const App = () => <List list={list} />;

 

const List = ({ list }) => (

<ul>

{list.map(item => (

<ListItem key={item.id} item={item} />

))}

</ul>

);

 

const ListItem = ({ item }) => (

<li>

<div>{item.id}</div>

<div>{item.firstname}</div>

<div>{item.lastname}</div>

<div>{item.year}</div>

</li>

);

When starting a new React project, I always tell people that it’s fine to have multiple components in one file, and it’s even tolerable in a larger React application when one component is strictly tight to another.

However, in this scenario, this one file will eventually become insufficient for your React project, so we’ll move on to step two.

Multiple React Files

The second phase follows this rule: Multiple files to rule them all. Consider our earlier App component with its List and ListItem components. Rather than having everything in one src/App.js file, we may divide these components into separate files. You pick how far you want to go here. For example, I would use the following folder structure:

– src/

— App.js

— List.js

 

While the src/List.js file contains the implementation details for the List and ListItem components, it only exports the List component as the file’s public API:

const List = ({ list }) => (

<ul>

{list.map(item => (

<ListItem key={item.id} item={item} />

))}

</ul>

);

 

const ListItem = ({ item }) => (

<li>

<div>{item.id}</div>

<div>{item.firstname}</div>

<div>{item.lastname}</div>

<div>{item.year}</div>

</li>

);

 

export { List };

Next, the src/App.js file can import the List component and use it:

import * as React from ‘react’;

 

import { List } from ‘./List’;

 

const list = [ … ];

 

const App = () => <List list={list} />;

To take things a step further, you could isolate the ListItem component into its own file and allow the List component to import the ListItem component.

– src/

— App.js

— List.js

— ListItem.js

However, as previously stated, this may go too far because the ListItem component is already strongly tied to the List component, and it would be OK to leave it in the src/List.js file. I use the rule of thumb that if a React component becomes reusable, I separate it out as a standalone file, as we did with the List component, to make it available to other React components.

From React Files to React Folders

From here, it gets more intriguing, but also more opinionated. Every React component gradually becomes more complex. Not only is additional functionality added (for example, more JSX with conditional rendering or logic with React Hooks and event handlers), but there are also more technical considerations, such as styles and testing. A basic way would be to include additional files next to each React component. For example, suppose each React component contains a test and a style file:

– src/

— App.js

— App.test.js

— App.css

— List.js

— List.test.js

— List.css

 

This clearly does not scale well, as each extra component in the src/ subdirectory causes us to lose track of each individual component. That’s why I like to have one folder for each React component.

– src/

— App/

—– index.js

—– component.js

—– test.js

—– style.css

— List/

—– index.js

—– component.js

—– test.js

—– style.css

While the new style and test files style and test each local component, the new component.js file contains the component’s actual implementation logic. What’s missing is the new index.js file, which represents the folder’s public interface and exports everything essential to the outside world. For example, for the List component, it typically looks like this:

export * from ‘./List’;

The App component has a component.js file that can still import the List component in the following manner:

import { List } from ‘../List/index.js’;

In JavaScript, we can omit the /index.js for the imports, because it’s the default:

import { List } from ‘../List’;

The file names are already opinionated: If file pluralization is needed, test.js can be changed to spec.js and style.css to styles.css. Furthermore, if you use Styled Components rather than CSS, your file extension may change from style.css to style.js.
Once you’re familiar to the naming convention for folders and files, you can just search for “List component” or “App test” in your IDE to access each one. In contrast to my personal preference for succinct file names, I admit that many people prefer to be more verbose with their file names.

– src/

— App/

—– index.js

—– App.js

—– App.test.js

—– App.style.css

— List/

—– index.js

—– List.js

—– List.test.js

—– List.style.css

Anyway, if you collapse all component folders, independent of the file titles, you get a very concise and obvious folder structure.

– src/

— App/

— List/

If a component has more technical concerns, such as extracting custom hooks, types (e.g., TypeScript defined types), stories (e.g., Storybook), utilities (e.g., helper functions), or constants (e.g., JavaScript constants) into dedicated files, you can scale this approach horizontally within the component folder.

 

– src/

— App/

—– index.js

—– component.js

—– test.js

—– style.css

—– types.js

— List/

—– index.js

—– component.js

—– test.js

—– style.css

—– hooks.js

—– story.js

—– types.js

—– utils.js

—– constants.js

If you want to make your List/component.js more lightweight by separating the ListItem component into its file, you might try the following folder structure:

– src/

— App/

—– index.js

—– component.js

—– test.js

—– style.css

— List/

—– index.js

—– component.js

—– test.js

—– style.css

—– ListItem.js

Again, you may go one step further by giving the component its own nested folder with any associated technical concerns, such as tests and styles.

– src/

— App/

—– index.js

—– component.js

—– test.js

—– style.css

— List/

—– index.js

—– component.js

—– test.js

—– style.css

—– ListItem/

——- index.js

——- component.js

——- test.js

——- style.css

Leave a comment