There are different ways to manage data in an application. One of the ways data is managed in a ReactJs application is using of concept of prop drilling. Prop drilling requires that data is passed through every parent component down to the child. This method can pose some form of cumbersome nature and may not be completely scalable.
In this article, we will be exploring the concept of Context API that makes it easier to manage application state. First we need to understand what context is, how it can be implemented in a React application and then we will build a prototype banking app for proper understanding. To have a proper understanding of this article, some knowledge of React and JavaScript will be required. Let’s first define Context. You should have some basic knowledge of ReactJs before going through this.
Context
Context makes available a way for data to be passed through the component tree without having to pass props through every level downwards. This means that data is managed in a global way. With Context, data can be set globally; while it is accessible directly by any of the children component without the need to pass it through every parent element. To put it simply, Context helps us to avoid nesting through different components level to access data.
Context has some APIs, namely: React.createContext, Context.Cosumer, Context.Provider etc. In this article, we will concentrate on React.createContext and Context.Provider.
Let’s create our application in the following steps. We will create a react application using create-react-app
We will use material UI component to style the various interfaces.
Our application is to show how state is managed across the deposit, balance and withdrawal screens of our financial application.
After creating the fakebank app with create-react-app, under the src folder we will create a folder called components, where all our folders including the context component will be stored. In the component folder, create a file context.js. The context folder is where the entire application state is being managed.
In the context component, some component will be imported from react; namely useState and useContext
import React from {useState, useContext}
Next we call in the createContext method, which provides a way for data to be passed through the component tree without having to manually pass props on every level.
import React from {useState, useContext}
import React, { useState, useContext } from "react";
const AppContext = React.createContext();
The next thing is to create the component via which the children props will be passed; we will also set the initial states of the balance, withdrawal and deposit screen respectively.
import React, { useState, useContext } from "react";
const AppContext = React.createContext();
const AppProvider = ({ children }) => {
const initialValues = { balance: 0, deposit: 0, withdrawal: 0 };
const [inputValue, setInputValue] = useState(initialValues);
let walletBalance = inputValue.balance;
let amountDeposit = inputValue.deposit;
let amountWithdrawn = inputValue.withdrawal;
The function above would return the children props wrapped with the AppContext.Provider. The Provider allows consuming components to subscribe to changes in context. Next, we will export our AppContext and AppProvider components. But first, we will set up a custom hook to make our code cleaner. (NOTE: You may decide not to use a custom hook)
import React, { useState, useContext } from "react";
const AppContext = React.createContext();
const AppProvider = ({ children }) => {
const initialValues = { balance: 0, deposit: 0, withdrawal: 0 };
const [inputValue, setInputValue] = useState(initialValues);
let walletBalance = inputValue.balance;
let amountDeposit = inputValue.deposit;
let amountWithdrawn = inputValue.withdrawal;
return (
<AppContext.Provider>
{children}
</AppContext.Provider>
);
}
//custom hook
const useGlobalContext = () => {
return useContext(AppContext);
};
export { useGlobalContext, AppProvider };
We will wrap the App component in the index.js file with the exported AppProvider.
<React.StrictMode>
<AppProvider>
<App />
</AppProvider>
</React.StrictMode>
Next is to pass the values of the initial state of our application to the AppProvider component.
import React, { useState, useContext } from "react";
const AppContext = React.createContext();
const AppProvider = ({ children }) => {
const initialValues = { balance: 0, deposit: 0, withdrawal: 0 };
const [inputValue, setInputValue] = useState(initialValues);
let walletBalance = inputValue.balance;
let amountDeposit = inputValue.deposit;
let amountWithdrawn = inputValue.withdrawal;
return (
<AppContext.Provider value={{
walletBalance,
amountDeposit,
amountWithdrawn,
}}>
{children}
</AppContext.Provider>
);
}
//custom hook
const useGlobalContext = () => {
return useContext(AppContext);
};
export { useGlobalContext, AppProvider };
Next we need to create the interfaces of the deposit, withdrawal and balance screens. As said earlier, we will be using the material UI framework to build our application interfaces.
import React from "react";
import {
Box,
Card,
CardContent,
Typography,
CardActions,
Button,
Divider,
} from "@mui/material";
const Deposit = () => {
return (
<Box
sx={{
marginTop: "10rem",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Card sx={{ minWidth: 700, background: "#A52A2A", color: "white" }}>
<CardContent>
<Typography
sx={{ fontSize: 20, textAlign: "center", fontWeight: "bold" }}
gutterBottom
>
Deposit
</Typography>
<Divider color="white" />
<Box
sx={{ display: "flex", justifyContent: "space-around", mt: "1rem" }}
>
<Typography sx={{ mb: 1.5, color: "white" }} color="text.secondary">
Balance
</Typography>
<Typography sx={{ mb: 1.5, color: "white" }} color="text.secondary">
$100
</Typography>
</Box>
<Typography sx={{ fontWeight: "bold" }}>Deposit Amount</Typography>
<form>
<input
type="text"
id="deposit"
name="deposit"
value="deposit"
/>
</form>
</CardContent>
<CardActions>
<Button
variant="contained"
sx={{ background: "white", color: "black" }}
>
Deposit
</Button>
</CardActions>
</Card>
</Box>
);
};
export default Deposit;
The interface above is for the deposit screen; the same process will be repeated for the other screens. The balance above is hardcoded with a sum of $100, this will be dynamically changes later on.
After creating the interfaces, in the context component, we need to create event handlers to handle the changes that handle input forms where the amount deposited and withdrawn will be typed; this will be handled in the context component.
import React, { useState, useContext } from "react";
const AppContext = React.createContext();
const AppProvider = ({ children }) => {
const initialValues = { balance: 0, deposit: 0, withdrawal: 0 };
const [inputValue, setInputValue] = useState(initialValues);
let walletBalance = inputValue.balance;
let amountDeposit = inputValue.deposit;
let amountWithdrawn = inputValue.withdrawal;
//handle input change
const handleChange = (e) => {
const { name, value } = e.target;
setInputValue({ ...inputValue, [name]: value });
};
return (
<AppContext.Provider
value={{
walletBalance,
amountDeposit,
amountWithdrawn,
handleChange,
}}
>
{children}
</AppContext.Provider>
);
};
//custom hook
const useGlobalContext = () => {
return useContext(AppContext);
};
export { useGlobalContext, AppProvider };
The custom hook will be destructured in each of the screens to pass the values in the context component.
const { walletBalance, amountDeposit, handleChange } =
useGlobalContext();
Below shows the values passed into the deposit component.
In this article, we have examined how the context API hook can be used to maintain state in a react application, the process of putting the app state in a single components therefore making it easy to pass data across different views.
import React from "react";
import {
Box,
Card,
CardContent,
Typography,
CardActions,
Button,
Divider,
} from "@mui/material";
import { useGlobalContext } from "./context";
const Deposit = () => {
const { walletBalance, amountDeposit, handleChange } =
useGlobalContext();
return (
<Box
sx={{
marginTop: "10rem",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Card sx={{ minWidth: 700, background: "#A52A2A", color: "white" }}>
<CardContent>
<Typography
sx={{ fontSize: 20, textAlign: "center", fontWeight: "bold" }}
gutterBottom
>
Deposit
</Typography>
<Divider color="white" />
<Box
sx={{ display: "flex", justifyContent: "space-around", mt: "1rem" }}
>
<Typography sx={{ mb: 1.5, color: "white" }} color="text.secondary">
Balance
</Typography>
<Typography sx={{ mb: 1.5, color: "white" }} color="text.secondary">
{walletBalance}
</Typography>
</Box>
<Typography sx={{ fontWeight: "bold" }}>Deposit Amount</Typography>
<form>
<input
type="text"
id="deposit"
name="deposit"
value={amountDeposit}
onChange={handleChange}
/>
</form>
</CardContent>
<CardActions>
<Button
variant="contained"
sx={{ background: "white", color: "black" }}
>
Deposit
</Button>
</CardActions>
</Card>
</Box>
);
};
export default Deposit;
This will be replicated on the other two screens, namely: withdrawal and balance.
The next thing is to handle the logic that keeps track of the deposit, amount and balance. Remember every logic is handled in the context component.
//handle incrementing the deposit made to update balance
const handleAmount = (e) => {
e.preventDefault();
if (regex.test(amountDeposit)) {
walletBalance += parseInt(amountDeposit);
inputValue.balance = walletBalance;
setInputValue({ ...inputValue, walletBalance });
amountDeposit = "";
inputValue.deposit = amountDeposit;
setInputValue({ ...inputValue, amountDeposit });
} else {
alert("You have entered an invalid character!!!");
}
};
The functions updates the balance once deposit is made.
//handle withdrawals
const withdrawalHandler = (e) => {
e.preventDefault();
if (regex.test(amountWithdrawn)) {
walletBalance -= parseInt(amountWithdrawn);
inputValue.balance = walletBalance;
setInputValue({ ...inputValue, walletBalance });
amountWithdrawn = "";
inputValue.withdrawal = amountWithdrawn;
setInputValue({ ...inputValue, amountWithdrawn });
} else {
alert("You have entered an invalid character!!!");
}
};
Pass the handleAmount and withdrawalHandler amount into the provider and get it destructured in their respective components.
<AppContext.Provider
value={{
walletBalance,
amountDeposit,
amountWithdrawn,
handleChange,
handleAmount,
withdrawalHandler,
}}
>
{children}
</AppContext.Provider>
The useGlobalContext on the withdrawal and deposit should look like the below:
const { walletBalance, amountDeposit, handleChange, handleAmount } =
useGlobalContext();
and
const { walletBalance, amountWithdrawn, handleChange, withdrawalHandler } =
useGlobalContext();
The balance components is thus:
import React from "react";
import {
Box,
Card,
CardContent,
Typography,
Divider,
} from "@mui/material";
import { useGlobalContext } from "./context";
const Balance = () => {
const { walletBalance } = useGlobalContext();
return (
<Box
sx={{
marginTop: "10rem",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Card sx={{ minWidth: 700, background: "#A52A2A", color: "white" }}>
<CardContent>
<Typography
sx={{ fontSize: 20, textAlign: "center", fontWeight: "bold" }}
gutterBottom
>
Balance
</Typography>
<Divider color="white" />
<Box
sx={{ display: "flex", justifyContent: "space-around", mt: "1rem" }}
>
<Typography sx={{ mb: 1.5, color: "white" }} color="text.secondary">
Balance
</Typography>
<Typography sx={{ mb: 1.5, color: "white" }} color="text.secondary">
{walletBalance}
</Typography>
</Box>
</CardContent>
</Card>
</Box>
);
};
export default Balance;
In this article we have been able to learn the React Context API in a very practical manner by building a financial app that helps keeps track record of deposit, amount and withdrawal. You can dig deeper into more detail by reading the official react documentation
Top comments (1)
This came at the right time welldone. What if I want to to make an API call within the context. Can you give an explanation on that?