Sharing data between components in Reactjs can sometimes be done by passing props however this can be a tedious process in large application with big component tree.
Reactjs has a convenient solution to such situations by a term called React Context. React Context allow us to share data between nested components easily without depending on props. The more important aspect with React Context is when you have multiple nested components and you want to send data deeper along with the components.
Â
Use Cases When Using Context is more convenient
- When you need to share data globally between multiple components and this data can be changed interactively between those components.
- When you have multiple nested components and each component receives the same props from the parent component.
Â
Use Cases When Using Props is more convenient
- When the data you need to send is not shared globally i.e just to send data to one level of components only.
Components of Reactjs Context
- Provider: The provider is a component which supplies data to the enclosed components, each component enclosed in this provider have access to the context data.
- Consumer: The consumer is a component that listens to data changes from provider component and provides a callback function to render the child components.
Creating context
To create a new context object call React.createContext() function:
let myContext = React.createContext(defaultValue);
The above code return a new context object, so when React renders a component that subscribes to this context object it will read the current context value from the closest matching Provider.
Â
Context Provider
Using the above context is a matter of of using the context provider component to enclose the components that we need to access the data in the components. Typical place to add the provider in a parent component.
const App = function(props) { return ( <myContext.Provider value=""> <ChildComponent /> </myContext.Provider> ) }
The value prop shown above in myContext.Provider represents the value you want to share. This can be for example passed from component local state. In case there is no value prop the defaultValue can be used.
One Provider can be connected to many consumers. Providers can be nested to override values deeper within the tree. All consumers that are descendants of a Provider will re-render whenever the Provider’s value
prop changes
Â
Context Consumer
Now the time to child components to access the data in the context, to achieve this by consuming the context which means subscribing to data changes in the context.
const ChildComponent = function(props) { return ( <myContext.Consumer> { value => // render something based on the value } </myContext.Consumer> ) }
As you see the consumer is a function which takes a single parameter which is value of the context data. In case the provider has no value the defaultValue will be used.
Example
Let’s take a real example of a simple application that allows users to change profile data such as username, photo. When this data is changed it must be changed in multiple places such as website header, profile page. I assume that we use twitter bootstrap here so let’s start.
App.js
import React from 'react'; import Navbar from './Navbar'; import EditProfile from './EditProfile'; function App() { return ( <div className="App container"> <div className="row"> <div className="col-md-12"> <Navbar /> <EditProfile /> </div> </div> </div> ); } export default App;
Navbar.js
import React from 'react'; class Navbar extends React.Component { render() { return ( <nav className="navbar navbar-default"> <div className="container-fluid"> <div className="navbar-header"> <a className="navbar-brand" href="#"> React Context </a> <p className="navbar-text">Signed in as Demo user</p> </div> <div className="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul className="nav navbar-nav navbar-right"> <li> <a href="#" role="button" aria-haspopup="true" aria-expanded="false">Demo user <img src="https://www.clipartwiki.com/clipimg/full/174-1742152_computer-icons-user-clip-art-transparent-user-icon.png" width="21" height="21" /></a> </li> </ul> </div> </div> </nav> ) } }; export default Navbar;
EditProfile.js
import React from 'react'; import Profile from './Profile'; class EditProfile extends React.Component { render() { return ( <div className="row"> <div className="col-md-12"> <h3>Edit profile</h3> </div> <div className="col-md-8"> <form method="post"> <div className="form-group"> <label>Username</label> <input type="text" className="form-control" name="username" placeholder="username" /> </div> <div className="form-group"> <label>Photo</label> <input type="text" className="form-control" name="photo" placeholder="insert photo url" /> </div> <button type="submit" className="btn btn-primary">Save</button> </form> </div> <Profile/> </div> ); } }; export default EditProfile;
Profile.js
import React from 'react'; class Profile extends React.Component { render() { return ( <div className="col-md-4"> <div className="thumbnail text-center"> <img className="img-circle" width="200" height="170" src="https://www.clipartwiki.com/clipimg/full/174-1742152_computer-icons-user-clip-art-transparent-user-icon.png" /> <span className="label label-default">Demo user</span> </div> </div> ) } }; export default Profile;
This is a simple page which contains a navbar in the header with the currently signed in user username and photo. second part of the page contains two blocks where the first block is a simple form which allow user to update his profile, as you see i the photo here is a full text url for simplicity. And the second block is the updated data that is shown after i save the form.
Now let’s create and apply the context:
AppContext.js
import React from 'react'; import ProfileData from './ProfileData'; const AppContext = React.createContext({ data: ProfileData, updateProfile: () => {} }); export default AppContext;
ProfileData.js
const ProfileData = { 'username': 'Demo user', 'photo': 'https://www.clipartwiki.com/clipimg/full/174-1742152_computer-icons-user-clip-art-transparent-user-icon.png' }; export default ProfileData;
Modify App.js
import React from 'react'; import Navbar from './Navbar'; import EditProfile from './EditProfile'; import AppContext from './AppContext'; import ProfileData from "./ProfileData"; class App extends React.Component { constructor(props) { super(props); this.state = { data: ProfileData, updateProfile: () => {} }; } render() { return ( <AppContext.Provider value={this.state}> <div className="App container"> <div className="row"> <div className="col-md-12"> <Navbar/> <EditProfile/> </div> </div> </div> </AppContext.Provider> ); } } export default App;
Navbar.js
import React from 'react'; import AppContext from './AppContext'; class Navbar extends React.Component { render() { return ( <AppContext.Consumer> { value => { return ( <nav className="navbar navbar-default"> <div className="container-fluid"> <div className="navbar-header"> <a className="navbar-brand" href="#"> React Context </a> <p className="navbar-text">Signed in as {value.data.username}</p> </div> <div className="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul className="nav navbar-nav navbar-right"> <li> <a href="#" role="button" aria-haspopup="true" aria-expanded="false">{value.data.username} <img src={value.data.photo} width="21" height="21"/></a> </li> </ul> </div> </div> </nav> ) } } </AppContext.Consumer> ) } }; export default Navbar;
EditProfile.js
import React from 'react'; import Profile from './Profile'; import AppContext from './AppContext'; import App from "./App"; class EditProfile extends React.Component { constructor(props) { super(props); this.state = { username: '', photo: '' }; this.handleUsername = this.handleUsername.bind(this); this.handlePhoto = this.handlePhoto.bind(this); this.handleSubmit = this.handleSubmit.bind(this); } handleUsername(e) { this.setState({ username: e.target.value }); } handlePhoto(e) { this.setState({ photo: e.target.value }); } handleSubmit(e) { } componentDidMount() { this.setState({ username: this.context.data.username, photo: this.context.data.photo }); } render() { return ( <div className="row"> <div className="col-md-12"> <h3>Edit profile</h3> </div> <div className="col-md-8"> <form method="post" onSubmit={this.handleSubmit}> <div className="form-group"> <label>Username</label> <input type="text" className="form-control" name="username" placeholder="username" value={this.state.username} onChange={this.handleUsername} /> </div> <div className="form-group"> <label>Photo</label> <input type="text" className="form-control" name="photo" placeholder="insert photo url" value={this.state.photo} onChange={this.handlePhoto} /> </div> <button type="submit" className="btn btn-primary">Save</button> </form> </div> <Profile/> </div> ); } }; EditProfile.contextType = AppContext; export default EditProfile;
Profile.js
import React from 'react'; import AppContext from './AppContext'; class Profile extends React.Component { render() { return ( <AppContext.Consumer> { value => { return ( <div className="col-md-4"> <div className="thumbnail text-center"> <img className="img-circle" width="200" height="170" src={value.data.photo}/> <span className="label label-default">{value.data.username}</span> </div> </div> ) } } </AppContext.Consumer> ) } }; export default Profile;
As you see above i created a context which have a default object value with object items the profile data and a callback function which we will see it shortly.
After that in the main App component i wrapped my component jsx with <AppContext.Provider /> component and add a value prop which is is component state. I set the state here to be equal to the context data payload.
Then on the child components i wrapped them with <AppContext.Consumer /> component which enable me to access the value within the context and thereby access the username and photo items.
In the EditProfile component i set EditProfile.contextType property = AppContext which enable me to access the context in component lifecycle hooks using this.context as i did in componentDidMount(). In fact this is another way to access the context without using <AppContext.Consumer /> component.
If you run the application you will see the data shown in both the header and the profile page.
Â
Submitting The Form and Updating the Context
Let’s submit the form and update the context, this is the other topic in this tutorial which is how to update the context from nested component. In this case the nested component is EditProfile and we want to submit the form and change the context data globally.
If we noted from above when we created the context object we passed an object as the default value which contains the profile data and a callback function. The point here is the callback function which we will use to update the context data.
Open App.js and modify the constructor as shown:
constructor(props) { super(props); this.updateProfile = (data) => { this.setState({ data: { username: data.username, photo: data.photo } }) }; this.state = { data: ProfileData, updateProfile: this.updateProfile }; }
Here we added the updateProfile() function which receives the data argument and update the state. This function will be called in EditProfile component as we will see below.
Remember that we assigned the context data to the component state, this allow us to update the main component when calling updateProfile() function.
Now the updateProfile() function is available in the context data and i can access it in EditProfile component with this.context.EditProfile() as shown below:
Modify EditProfile.js component handleSubmit() function like this:
class EditProfile extends React.Component { ..... .... handleSubmit(e) { e.preventDefault(); this.context.updateProfile(this.state); } .... .... }