React.js with TypeScript: Sharing Data Across Components using React.Context

In this article, we will see the step-by-step implementation of using "React.Context" to share data across components. We will use TypeScript as a programming language to implement the code for this article. 

 Why TypeScript?

TypeScript is an open-source language that increases JavaScript application productivity by providing a type system. TypeScript is a typed JavaScript that compiles to plain JavaScript. TypeScript delivers a unique and easy approach to building front-end applications.

React Context     

React.js is a JavaScript Library for building interactive UI. This library is used to build the composable UI. In such cases, the UI may contain multiple components and these components may have a Parent-Child relationship across them. With the UI having such type of relationship, we may have the case of sharing data across the components. The Context provides a way to pass data through the components tree without having to pass props down manually at every level. We use the Context to share the data that can be considered global for React component Tree. Figure 1 shows the use of Context in React application.



Figure 1: Use of the Context   

As shown in Figure 1, the Context object defines a schema that contains data that is provided by the Provider Component (aka Parent Component). The Child Component consumes data from the Context using the useContext() hook. The Context object is created using the createContext() function. This function returns the generic Context<T> object.  Here the T is the type of data that is shared across components.

The Implementation

To implement the code, we need Microsoft Visual Studio Code (VSCode) and Node.js.

Step 1: Open the Command Prompt or Terminal Window. Create a directory where the project can be created. Navigate to this directory and run the following command to create the React application using TypeScript

npx create-react-app my-app --template typescript

Step 2: Navigate to the my-app folder. In this folder, we will have the src sub-folder that contains all the code. In the src folder, add a new folder and name it models. In this folder, we will add a new code file named productmodel.ts and in this file, we will add the Product class as shown in Listing 1


export class Product {
    [x:string]:any; 
    constructor(
        public ProductId:number,
        public ProductName:string,
        public CategoryName:string,
        public Manufacturer: string,
        public Price: number
    ){}
}

Listing 1: The Product class   

Note that the Product class defines an indexer x. This is used to read properties of the class using Object.keys() and iterate over it by using the property name as an index. This is required in TypeScript.

Step 2: In the src folder, add a new folder and name it as components. In this folder, add a new folder and name it as productcomponent (generally we should put each component in a separate folder). In the productcomponent folder add a new file and name it as productcomponent.tsx. In this file, we will add code for ProductComponent that will define the Product state to accept product information from the end-user. The accepted product information then will be stored in the Products array state and we will generate an HTML table to show the product's data. The code for the ProductComponent is shown in Listing 2


import React, {useState} from 'react'

import DataTableComponent from './datatablecomponent';
import { Product } from '../../models/productmodel'

import { DataContextEvent } from '../../dataotext';

const ProductComponent = () => {
  
    /* define a prodiuct state */
    const [product, setProduct] = useState<Product>({
        ProductId:0, ProductName:'', CategoryName:'', Manufacturer:'', Price:0
    });
    const [products, setProducts] = useState<Product[]>([]);
    
    /* Local Constants for showing data in select elements */

    const categories:string[] = ['Electronics', 'Electrical', 'Home', 'Fashion'];

    const manufacturers = ['TATA', 'Bajaj', 'Reliance', 'Phillipse'];

    const clear=()=>{
        setProduct({
            ProductId:0, ProductName:'', CategoryName:'', Manufacturer:'', Price:0
        });
    };

    const save=()=>{
        setProducts([...products, product]);
    };

  return (
    <div className='container'>
       <div className='form-group'>
           Product Id:
           <input type="text" placeholder='Product Id' className='form-control'
            value={product.ProductId}
            onChange={(evt)=>setProduct({...product, ProductId:parseInt(evt.target.value)})}
           />
       </div>
       <div className='form-group'>
           Product Name:
           <input type="text" placeholder='Product Name' className='form-control'
            value={product.ProductName}
            onChange={(evt)=>setProduct({...product, ProductName:evt.target.value})}
           />
       </div>
       <div className='form-group'>
           Category Name:
           
           <select  className='form-control' title='CategoryName'
            value={product.CategoryName}
            onChange={(evt)=>setProduct({...product, CategoryName:evt.target.value})}
           >
             <option>Select Category</option>
             {
                categories.map((cat,idx)=>(
                    <option key={idx} value={cat}>{cat}</option>
                ))  
             }

           </select>
       </div>
       <div className='form-group'>
           Manufacturer Name:
           
           <select  className='form-control' title='Manufacturer'
            value={product.Manufacturer}
            onChange={(evt)=>setProduct({...product, Manufacturer:evt.target.value})}
           >
            <option>Select Manufacturer</option>
             {
                manufacturers.map((man,idx)=>(
                    <option key={idx} value={man}>{man}</option>
                ))  
             }

           </select>
       </div>
       <div className='form-group'>
           Product Id:
           <input type="text" placeholder='Price' className='form-control'
            value={product.Price}
            onChange={(evt)=>setProduct({...product, Price:parseInt(evt.target.value)})}
           />
       </div>
       <div className='form-group'>
           <button className='btn btn-warning'
             onClick={clear}
           >Clear</button>
           <button className='btn btn-success'
            onClick={save}
           >Save</button>
 
       </div>
       <br/>
         
       <table className='table table-bordered table-striped'>
          <thead>
            <tr>
                {
                    Object.keys(product).map((header,index)=>(
                        <th key={index}>{header}</th>
                    ))
                }
            </tr>
          </thead> 
          <tbody>
            {
                products.map((prd,idx)=>(
                    <tr key={idx}
                      onClick={()=>setProduct(prd)}
                    >
                        {
                            Object.keys(product).map((header,index)=>(
                                <td key={index}>{prd[header]}</td>
                            )) 
                        }
                    </tr>
                ))
            }
          </tbody>
       </table>      

    </div>
  )
}

export default ProductComponent

Listing 2: The ProductComponent

Carefully observe the code in Listing 2, we have declared the product state property using the userState<T> hook. Here the T is the Product class. We are also generating HTML select element options based on the categories and the manufacturers' array. The save() method adds the product object to the products state property. The component generates the HTML table based on the data from the products array. Carefully see the code for generating HTML table rows, here we are using the Object.keys() to generate table cells and show product data in it for each property of the Product class. Here we are using the property name of the product class as an index to read its value as I explained in the code of Listing 1. The HTML table row is bound with the onClick event so that when the row is clicked the selected row cell values will be shown in the text elements as well as in the select element.  

Step 3: Modify the index.ts to mount the ProductComponent as shown in Listing 3


......
root.render(
  <React.StrictMode>
    <ProductComponent/>
  </React.StrictMode>
);
......

Listing 3: Index.ts to mount the ProductComponent

To run the application run the following command Command prompt or Terminal window  

npm run start

The browser will show component as shown in Figure 1



Figure 1: The Component in the browser

Enter data for the Product and click on the Save button, the product data will be shown in the HTML table. Click on the Clear button to clear entries in text elements and select elements. Now click on the table row, the row values will be shown in text elements and the select option will be shown for CategoryName and Manufacturer.            

So far we have created a component so now the question here is where we are going to use the Context for data sharing? To answer this, let's consider that our application requires an HTML table across multiple components to show the list of the data e.g. Products, Orders, Customers, etc. in OrderComponent, CustomerComponent, etc. Then to generate an HTML table in each component again and again it is better to have a reusable TableComponent. This will make the application more maintainable and that is the use of WebComponent-based JavaScript Libraries and Frameworks.  We will be using this TableComponent as a child for each component and each parent component will pass the data to this TableComponet using the Context.    

Step 4: Let's add a new file in the src folder and name it as datacontext.ts. In this file, we will create a Context object using createContext() function as shown in Listing 4.

import { createContext } from "react";
import { Product } from "./models/productmodel";


/* If the Context object is used for
  complex data as well as dispatch event
  then make sure that the context object should not have
  the default value as an empty {} object
*/

/* define a schema for the Context */

interface DataContextSchema {
    [x:string]:any,
    products:Array<any>,
    setProduct: React.Dispatch<React.SetStateAction<Product>> | null
}

/* Define a DataContext */
export const DataContextEvent = createContext<DataContextSchema>({
    products:[],
    setProduct:null
});


Listing 4: The Context object

The code in Listing 4 defines the DataContextEvent context object using the createContext<T>() function. Here T is the DataContextSchema interface type. This interface defines the products property of an array type and setProduct property of type React.Dispatch. This means that this context object shares the products array from parent to child component. The React.Dispatch means that the child component will dispatch an action so that the parent component can receive the data emitted from it.      

Step 5: In the productcomponent folder, add a new file named datatablecomponent.tsx. In this file, we will add code for DataTableComponent. This component will consume the data from the Context. This component will use the useContext() hook to consume context object and read data from it. Listing 5 shows the code for the DataTableComponent.


/* Import useContext so that the current component will consume
 data received from the parent using the Context Object
*/
import React, {useContext} from 'react'
/* Use the Context Schema */
import { DataContextEvent } from '../../datacotext';

const DataTableComponent = () => {
  // 1. Create Consumer from the context
  let consumer = useContext(DataContextEvent);  

  // 2. Read array data from it
  let dataSource:Array<object> = new Array<object>();

  dataSource = consumer[Object.keys(consumer)[0]]; //This will read products

  // 3. Read the Callback Dispatch Action

  let event = consumer[Object.keys(consumer)[1]]; // This will read setProduct()

  // 4. Make sure that the dataSource is in the valid state
  if(dataSource === undefined || dataSource === null || dataSource.length === 0){
    return (
        <div className='alert alert-danger'>
            <strong>
                The Data is not presently shown in the component
            </strong>
        </div>
    );
  }

  // 5. Make sure that the dataSource is an array so that columns and rows can be generated
  if(!Array.isArray(dataSource)){
    return (
        <div className='alert alert-danger'>
            <strong>
                No Records
            </strong>
        </div>
    );
  } else {
    let columns = Object.keys(dataSource[0]);
  return (
    <div className='container'>
      <table className='table table-bordered table-striped'>
          <thead>
            <tr>
                {
                    columns.map((col,idx)=>(
                        <th key={idx}>{col}</th>
                    ))
                }
            </tr>
          </thead> 
          <tbody>
            {/* Make sure that when rows are generated
              the column (col) MUST be explicitly set as 
              the 'key' of the 'record' using
              typescript 'keyof' definition

              'col as keyof typeof record'

              The 'col' is a property of the 'record' object


            */}
            {
                dataSource.map((record:object,index)=>(
                     <tr key={index} onClick={()=>event(record)}>
                        {
                            columns.map((col,idx)=>(
                                <td key={idx}>{record[col as keyof typeof record]}</td>
                            ))
                        }
                     </tr>
                ))
            }
          </tbody>
       </table>      
    </div>
  )}
}

export default DataTableComponent;

Listing 5: The DataTableComponent             

Carefully look into the code of Listing 5. The useContext() hook is used to consume the context object. Since this context object has an array property and React.Dispatch() property, the consumer object reads them in this component. The dataSource object read the array data and the event object read the React.Dispatch(). The component must check if the dataSource is not undefined, null, or empty. If it is not valid then the component must return fallback UI otherwise the table can be generated based on data received from the context. The HTML table is generated using the dataSource object. The Table row is bound with the event received from the context so that when the table row is clicked the row data is emitted back to the parent component.

Step 6: Modify the code of the ProductComponent to use the DataContextEvent context object and the DataTableComponent as shown in Listing 6

import React, {useState} from 'react'
import DataTableComponent from './datatablecomponent';
import { Product } from '../../models/productmodel'

import { DataContextEvent } from '../../datacontext';

const ProductComponent = () => {
  
    /* define a prodiuct state */
    const [product, setProduct] = useState<Product>({
        ProductId:0, ProductName:'', CategoryName:'', Manufacturer:'', Price:0
    });

    

   

    const [products, setProducts] = useState<Product[]>([]);
    
    /* Local Constants for showing data in select elements */

    const categories:string[] = ['Electronics', 'Electrical', 'Home', 'Fashion'];

    const manufacturers = ['TATA', 'Bajaj', 'Reliance', 'Phillipse'];

    const clear=()=>{
        setProduct({
            ProductId:0, ProductName:'', CategoryName:'', Manufacturer:'', Price:0
        });
    };

    const save=()=>{
        setProducts([...products, product]);
    };

  return (
    <div className='container'>
       <div className='form-group'>
           Product Id:
           <input type="text" placeholder='Product Id' className='form-control'
            value={product.ProductId}
            onChange={(evt)=>setProduct({...product, ProductId:parseInt(evt.target.value)})}
           />
       </div>
       <div className='form-group'>
           Product Name:
           <input type="text" placeholder='Product Name' className='form-control'
            value={product.ProductName}
            onChange={(evt)=>setProduct({...product, ProductName:evt.target.value})}
           />
       </div>
       <div className='form-group'>
           Category Name:
            
           <select  className='form-control' title='CategoryName'
            value={product.CategoryName}
            onChange={(evt)=>setProduct({...product, CategoryName:evt.target.value})}
           >
             <option>Select Category</option>
             {
                categories.map((cat,idx)=>(
                    <option key={idx} value={cat}>{cat}</option>
                ))  
             }

           </select>
       </div>
       <div className='form-group'>
           Manufacturer Name:
           
           <select  className='form-control' title='Manufacturer'
            value={product.Manufacturer}
            onChange={(evt)=>setProduct({...product, Manufacturer:evt.target.value})}
           >
            <option>Select Manufacturer</option>
             {
                manufacturers.map((man,idx)=>(
                    <option key={idx} value={man}>{man}</option>
                ))  
             }

           </select>
       </div>
       <div className='form-group'>
           Product Id:
           <input type="text" placeholder='Price' className='form-control'
            value={product.Price}
            onChange={(evt)=>setProduct({...product, Price:parseInt(evt.target.value)})}
           />
       </div>
       <div className='form-group'>
           <button className='btn btn-warning'
             onClick={clear}
           >Clear</button>
           <button className='btn btn-success'
            onClick={save}
           >Save</button>
 
       </div>
       <br/>
         {/* Using the Context to pass complex data to child */}

         <DataContextEvent.Provider value={{products,setProduct}}>
              <DataTableComponent></DataTableComponent>
        </DataContextEvent.Provider>  



        

    </div>
  )
}

export default ProductComponent


Listing 6: The ProductComponent with DataTableComponent

Have a careful look at the code in Listing 6. The Provider property of the Context object is used to share the products array and the setProduct dispatch action to the DataTable component. This means that when the products array is changed in the ProductComponent, it will share the data with the DataTableComponent and this component will generate an HTML table with product data in it. Similarly, when the table row is clicked in the DataTableComponent the setProduct action will be dispatched and the table row data will be shared with the ProductComponent and data will be shown in the text elements and in the select element option.

Install the React PlugIn for the browser from the web store where we can see the DataTableComponent mounted along with the ProductComponent as shown in Figure 2



Figure 2: The Mounted Components

 Figure 2 shows that since there is no data to show in the table in DataTableComponent, it is showing a fallback UI. We can also see that the Context.Provider is used by the ProductComponent to share data with the DataTableComponent.  Enter data for the Product and click on the Save button, the table will show data in it as shown in Figure 3



Figure 3: Data with Context        

 

That's it, the data is shared with the DataTableComponent. Now click on the Clear button the text elements will be cleared. Now click on the table row the selected table row values will be shown in text elements and select options.


Conclusion: The React.Context is the best way to share data from the parent with the specific child element.    



 

    

Popular posts from this blog

Uploading Excel File to ASP.NET Core 6 application to save data from Excel to SQL Server Database

ASP.NET Core 6: Downloading Files from the Server

ASP.NET Core 6: Using Entity Framework Core with Oracle Database with Code-First Approach