<aside> ℹ️ For more information on the specifics of the React context, please refer to the official documentation.
</aside>
The React Context API is a solution to the props-drilling problem. It enables the passing of data through the component tree without the need to pass props manually at each level.
To implement this solution is necessary to create a context file that defines the context and provider. The provider defines the states that children's components can access.
With ScandiPWA, you can easily wrap the App
component with the provider by extending the contextProviders
property to include the provider, which allows the context data to be accessed in every ScandiPWA component. Another approach is to extend a function that renders a component closer to where the context will be used and wrap it with the context provider.
To access data from the context provider in a newly created class component, you must define the desired context in the contextType
property of the component. However, this may not always be possible, especially if the component needs to use another context or if you need to extend a component. In such cases, you can use the context consumer to wrap the component and pass the desired states as props.
Context API is an integrated React tool that provides a way to pass data through the component tree without having to pass props down manually at every level(also known as props-drilling).
In a typical React application, data is passed top-down (parent to child) via props, but such usage can be cumbersome for certain types of props (e.g. locale preference, UI theme) that are required by many components within an application.
In the given image example, the A
component is a descendant of the contextProvider
. The contextProvider
defines dState
and updateDState
, which can be accessed in all descendant components without the need for passing them through every level.
Context API is a good solution when there is a close common parent among the used components or the scope of implementation is very clear. For example, the states will only be used in an extension with new components.
<aside> ✅ For more information on the specifics of React context, please refer to the official documentation.
</aside>
<aside> ⚠️ Exp
</aside>
To create a context and its provider, follow these steps:
Create a context file named <entity>.context.js
in the src/context/
folder.
Define the new context:
You have to use createContext
from React
library to create the context.
import { createContext } from 'react';
export const ExampleContext = createContext({
// Can add default states here later
});
ExampleContext.displayName = 'ExampleContext';
You should give a descriptive name to your context, in general <ENTITY>Context
is a good context name. For example AmazonContext
in a file named AmazonContext.js
.
ExampleContext.displayName
defines the display name of the context in the component tree of the React DevTools.
The createContext
object parameter is used only when a component does not have a matching Provider above it in the tree. Therefore, if you plan to always inject the provider, this can be empty.
For example, you can add some states inside:
import { createContext } from 'react';
export const ExampleContext = createContext({
isLoading: false,
setIsLoading: () => {},
});
ExampleContext.displayName = 'ExampleContext';
Define the provider component
The provider of a context will allow its descendants to access the context:
import { createContext } from 'react';
import { ChildrenType } from 'Type/Common';
export const ExampleContext = createContext({});
ExampleContext.displayName = 'ExampleContext';
export const ExampleProvider = ({ children }) => {
return (
<ExampleContext.Provider>
{ children }
</ExampleContext.Provider>
);
};
ExampleProvider.displayName = 'ExampleProvider';
ExampleProvider.propTypes = {
children: ChildrenType.isRequired
};
This creates a provider component that returns a provider for the context you defined in the previous step. A good naming convention for providers is to use <ENTITY>Provider
, for example, AmazonProvider
.
This ExampleProvider.propTypes
example defines the children
prop of your provider component as required. You can also define the displayName
of the component.
But why use this intermediate component? You can define states and functions within the intermediate component and pass them to the context provider.
import { createContext, useState } from 'react';
import { ChildrenType } from 'Type/Common';
export const ExampleContext = createContext({});
ExampleContext.displayName = 'ExampleContext';
export const ExampleProvider = ({ children }) => {
const [isLoading, setIsLoading] = useState(false);
const value = {
isLoading,
setIsLoading
};
return (
<ExampleContext.Provider value={ value }>
{ children }
</ExampleContext.Provider>
);
};
ExampleProvider.displayName = 'ExampleProvider';
ExampleProvider.propTypes = {
children: ChildrenType.isRequired
};
The value
object will define all states and functions that you want to provide to the descendant components.
<aside> ⚠️ This section assumes that you have already created a context and context provider.
</aside>
To use your context in a component, you must ensure that this component is wrapped by the intended context provider.
In ScandiPWA, a common approach is to use plugins to wrap the App
component or a component that is closer to where the context is intended to be used.
<aside> ℹ️ Remember to follow the best practices when creating plug-in files to keep consistency and improve readability and maintenance!
</aside>
App
component with the context provider?desired context in the contextType
The App
component was developed to easily handle this situation. It has a property contextProviders
, which is used to define the context providers that will wrap its children.
You can easily inject your provider in the App
component contextProviders
property from App/Component
through a plugin:
import { ExampleProvider } from '../../context/Example';
const addExampleContextProvider = (member) => [
(children) => (
<ExampleProvider>
{ children }
</ExampleProvider>
),
...member
];
export default {
'Component/App/Component': {
'member-property': {
contextProviders: addExampleContextProvider
}
}
};
You may not need to wrap the App
component, and a plugin to a closer parent will be enough:
import { ResultsProvider } from '../../context/Results/Results.provider';
const wrapWithResultsProvider = (args, callback) => (
<ResultsProvider>
{ callback(...args) }
</ResultsProvider>
);
export default {
'Route/SearchPage/Component': {
'member-function': {
renderContent: wrapWithResultsProvider
}
}
};
This example wraps the renderContent
function of the SearchPage
component, and only the components rendered by it, or its descendants can access the provider data.
For a component to access the context data, it must be a descendant of the context provider it wants to access and consume it. In a class component, this is done by setting the propertycontextType
, but it has some limitations, and an alternative is to wrap the component you want to use the data with a context provider.
To access the context within a class component or container, you need to define the contextType
member of the class and set it as the corresponding context object.
import { ExampleContext } from '../../context/ExampleContext';
export class ExampleClassComponent extends React.PureComponent {
static contextType = ExampleContext;
componentDidMount(){
const { example } = this.context;
}
//...
}
export default ExampleClassComponent;
This.context
gives access to the values defined in the context provided. If the context is the same as defined in the create context section, it gives access to the isLoading
and setIsLoading
.
new context consumer, which will provide
value object
It's possible that the component you want to extend does not define the contextType
with the context you want to use, or it has already been defined with another context.
To solve this problem by the provider.
<aside>
ℹ️ The Context.Consumer
is a React component that subscribes to context changes. Using this component lets you subscribe to a context within a function component.
</aside>
To access the values of the context, you need to wrap the component with <ENTITY>Context.Consumer
and pass the value as props. For example:
import { ExampleContext } from '../../context/ExampleContext';
<ExampleContext.Consumer>
{ (value) => (<ExampleComponent context={ value } />) }
</ExampleContext.Consumer>
How to extend a component to add the Consumer
values to a component?
Overriding a component and its props like in the above example may not be a good idea, as it could affect the component's children and their props.
You can extend a function that renders the component to clone the component and add the provider values to its props:
import { cloneElement } from 'react';
import { GoogleAddressContext } from '../../context/GoogleAddress';
const wrapWithGoogleAddressContextConsumer = (args, callback) => {
const CheckoutAddressComponent = callback(...args);
return (
<GoogleAddressContext.Consumer>
{ ({ address }) => cloneElement(
CheckoutAddressComponent,
{
...CheckoutAddressComponent.props,
googleAddress: address
},
CheckoutAddressComponent.props.children
) }
</GoogleAddressContext.Consumer>
);
};
export default {
'Component/CheckoutAddressBook/Container': {
'member-function': {
render: wrapWithGoogleAddressContextConsumer
}
}
};
This example makes use of the cloneElement
function to clone the original component rendered by the callback, and add the Consumer
state address
value as props to it.
This was necessary because the CheckoutAddressBookContainer
does not specify the desired context in the contextType
property.
In order to pass a request result as props to the container, you have to store the result in the state then access it in the container.
You can achieve this either by using Context or Redux Store.
<aside> ℹ️ Learn more about Redux here! How to Work with Redux?
</aside>
With Context, you can simply create an asynchronous function which will call your request, then set it in the context’s state.
For example:
ExampleContext.context.js
<aside>
ℹ️ Remember that in order to successfully store the result in state, you must execute the fetchRequestResults
somewhere, in the container for example!
export class MyComponentContainer extends PureComponent {
static contextType = MyContext;
containerFunctions = {};
// Upon construction of the container, execute the function
// which calls the context's fetchRequestResults().
__construct(){
this.getQueryResults();
}
containerProps() {
// this destructures the stored state which will be containing
// the desired request results.
// This makes the request results available to be accessed in component props
const { requestResults } = this.context;
return { ...requestResults };
}
// this method in the container class will retrieve
// fetchRequestResults from the context and execute it.
getQueryResults() {
const { fetchRequestResults } = this.context;
fetchRequestResults();
}
</aside>