<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.

What is the Context API?

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>

Untitled

<aside> ⚠️ Exp

How to Work with Redux?

</aside>

How to create a context and its provider?

To create a context and its provider, follow these steps:

  1. Create a context file named <entity>.context.js in the src/context/ folder.

  2. 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';
    
  3. 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.

How to inject a context provider?

<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.

How to work with plugins?

<aside> ℹ️ Remember to follow the best practices when creating plug-in files to keep consistency and improve readability and maintenance!

</aside>

How to wrap the 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
        }
    }
};

How to wrap components with the context provider?

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.

How to access the context in a component?

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.

How to access the context in a class component or container?

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

How to access context when extending a component?

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.

How to make request results available as props in the container with context?

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:

<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>