Education
Developer Advocate
Last updated onNov 1, 2023
Last updated onAug 8, 2023
Welcome to another blog post by me! Today, we're going to dive into the world of Context API. If you've been scratching your head trying to understand what it is and how it works, you're in the right place.
The Context API in React is a great tool that allows us to share data across several components without having to explicitly feed props down at each level. It's a game-changer for complex apps where state management can be a nightmare.
In this post, we'll explore the ins and outs of the Context API, compare it with Redux, and discuss React Query vs Redux. We'll also delve into data fetching, managing state, and possible performance optimizations. So, buckle up, and let's get started!
Context API is a feature in React that provides a way to pass data through the component tree without having to pass props down manually at every level. It's designed to share data that can be considered "global" for a tree of React components, such as the current theme, or a user authentication status.
Context API is particularly useful when you have data that needs to be accessed by many components at different nesting levels. Instead of passing props down all the components in the tree or resorting to more complex state management libraries like Redux, you can use Context API to share data across all the components.
At the heart of Context API are two key parts: the Provider component and the Consumer component. The Provider component accepts a value prop and makes it available to all the components in its tree. The Consumer component, on the other hand, is used to consume the data provided by the Provider.
Let's take a look at a simple example:
1 import React, { createContext, useContext } from 'react'; 2 3 // Create a Context object 4 const MyContext = createContext(defaultValue); 5 6 function MyComponent() { 7 // Use the Context object 8 const contextValue = useContext(MyContext); 9 10 // Render something based on the context value 11 return <p>{contextValue}</p>; 12 } 13 14 function MyApp() { 15 return ( 16 // Use a Provider to pass the current context value to the tree below 17 <MyContext.Provider value="Hello, Context!"> 18 <MyComponent /> 19 </MyContext.Provider> 20 ); 21 } 22
In this example, we create a Context object using createContext(). We then use a Provider to pass the current context value ("Hello, Context!") to the tree below. Any component in the tree can access this value using the useContext() hook, as we did in MyComponent.
Context API is a great solution when you need to avoid prop drilling, i.e., passing data through intermediate components. However, it's not a silver bullet for all state management problems. For complex state management, other libraries like Redux or React Query might be more suitable. We'll discuss these in the upcoming sections.
As we've touched upon, the Provider component and the Consumer component are the two key players in Context API. The Provider component accepts a value prop and makes it available to all the components in its tree. This value can be anything - a single value, an object, or even a function.
1 <MyContext.Provider value={{ theme: "dark", toggleTheme: this.toggleTheme }}> 2 <MyComponent /> 3 </MyContext.Provider> 4
In the example above, we're passing an object with two properties to the value prop. Any component under this Provider can now access this data.
On the other hand, the Consumer component is used to consume the data provided by the Provider. It subscribes to context changes and re-renders whenever the Provider's value prop changes.
Passing data with Context API is straightforward. First, you wrap your component tree with the Provider component and pass the data you want to share via the value prop. Then, in any component where you need that data, you use the Consumer component or the useContext() hook.
1 const value = useContext(MyContext); 2
In the example above, we're using the useContext() hook to consume the context value. This hook returns the current context value for a given context, as given by the nearest matching Provider up the tree from the component in which the hook is called.
The Context object is what ties the Provider and Consumer components together. When you create a Context object using React.createContext(), it comes with a Provider and a Consumer component. The Provider component is used to "provide" data to all the components in its subtree, while the Consumer component is used to "consume" this data.
The Context object is also important because it allows you to set a default value for the context. This default value is used when a component doesn't have a matching Provider above it in the tree. This can be useful for testing components in isolation without wrapping them in a Provider.
1 const MyContext = React.createContext('default value'); 2
In the example above, we're creating a Context object with a default value of 'default value'. If a component uses this Context but doesn't have a matching Provider above it in the tree, it will use this default value.
Redux is a predictable state container for JavaScript applications. It's not specific to React, but it's often used with it. Redux helps you manage global state - data that is used across many components or your entire app. It's a powerful tool for complex apps where you need more control over the data flow.
Redux maintains the global state of an application in a single immutable state tree. You can't directly change the state; instead, you need to dispatch actions that tell Redux what to do. These actions are then handled by functions called reducers, which determine how the state should change in response to the action.
1 import { createStore } from 'redux'; 2 3 // This is a reducer - a function that takes the current state and an action, 4 // and returns a new state 5 function counter(state = 0, action) { 6 switch (action.type) { 7 case 'INCREMENT': 8 return state + 1; 9 case 'DECREMENT': 10 return state - 1; 11 default: 12 return state; 13 } 14 } 15 16 // Create a Redux store to manage your app's state 17 let store = createStore(counter); 18 19 // Dispatch actions to the store to change the state 20 store.dispatch({ type: 'INCREMENT' }); 21 store.dispatch({ type: 'DECREMENT' }); 22
In the example above, we create a Redux store with a simple counter reducer. We then dispatch actions to increment and decrement the counter.
While Redux and Context API both allow you to manage global state, they are different in many ways. Redux is a powerful tool for complex apps where you need more control over the data flow. It provides a single, immutable state tree that you can access from any component, a clear data flow, and a lot of flexibility.
On the other hand, Context API is built into React and is great for passing down data to deeply nested components. It's simpler and lighter than Redux, making it a good choice for smaller apps or features. However, it doesn't provide the same level of control over the data flow as Redux.
Redux is a predictable state container for JavaScript applications. It's not specific to React, but it's often used with it. Redux helps you manage global state - data that is used across many components or your entire app. It's a powerful tool for complex apps where you need more control over the data flow.
Redux maintains the global state of an application in a single immutable state tree. You can't directly change the state; instead, you need to dispatch actions that tell Redux what to do. These actions are then handled by functions called reducers, which determine how the state should change in response to the action.
1 import { createStore } from 'redux'; 2 3 // This is a reducer - a function that takes the current state and an action, 4 // and returns a new state 5 function counter(state = 0, action) { 6 switch (action.type) { 7 case 'INCREMENT': 8 return state + 1; 9 case 'DECREMENT': 10 return state - 1; 11 default: 12 return state; 13 } 14 } 15 16 // Create a Redux store to manage your app's state 17 let store = createStore(counter); 18 19 // Dispatch actions to the store to change the state 20 store.dispatch({ type: 'INCREMENT' }); 21 store.dispatch({ type: 'DECREMENT' }); 22
In the example above, we create a Redux store with a simple counter reducer. We then dispatch actions to increment and decrement the counter.
While Redux and Context API both allow you to manage global state, they are different in many ways. Redux is a powerful tool for complex apps where you need more control over the data flow. It provides a single, immutable state tree that you can access from any component, a clear data flow, and a lot of flexibility.
On the other hand, Context API is built into React and is great for passing down data to deeply nested components. It's simpler and lighter than Redux, making it a good choice for smaller apps or features. However, it doesn't provide the same level of control over the data flow as Redux.
React Query is a relatively new library that provides hooks for fetching, caching, synchronizing, and updating server state in your React applications. It's not a state management library in the traditional sense, but it's a powerful tool for managing async or server state.
React Query has several features that make it stand out, such as automatic caching, background updates, and stale data refetching. It also provides devtools for inspecting the state of your queries and mutations.
1 import { useQuery } from 'react-query'; 2 3 function MyComponent() { 4 const { isLoading, error, data } = useQuery('todos', fetchTodos); 5 6 if (isLoading) return 'Loading...'; 7 if (error) return 'An error occurred: ' + error.message; 8 9 return ( 10 <ul> 11 {data.map(todo => ( 12 <li key={todo.id}>{todo.title}</li> 13 ))} 14 </ul> 15 ); 16 } 17
In the example above, we're using the useQuery() hook from React Query to fetch todos. The hook returns an object with fields for the loading state, any error that occurred, and the data.
React Query shines when it comes to fetching, caching, and synchronizing server state. It automatically caches data, dedupes multiple requests for the same data, and keeps the data fresh by refetching it in the background when the window refocuses or the network reconnects. It also provides pagination and infinite loading features out of the box.
React Query also provides a lot of flexibility and control. You can customize the fetching, caching, and retrying behavior to suit your needs. And with the React Query Devtools, you can inspect the state of your queries and mutations in real time.
While Redux is a general-purpose state management library, React Query is specifically designed for managing server state. This makes React Query a great choice for fetching, caching, and synchronizing server state, while Redux might be a better choice for client state or complex state management.
That being said, Redux and React Query can be used together in the same app. You can use Redux for client state and React Query for server state, and they will coexist peacefully.
Data fetching is a crucial part of any modern web application. In React, there are several ways to fetch data from a server or an API. You can use the built-in fetch() function, or you can use a library like axios. However, fetching data is only part of the story. You also need to manage the fetched data, handle loading and error states, and possibly cache the data for performance.
Fetching data in a React component involves three steps: initiating the fetch, rendering the fetched data, and handling any errors. Here's a basic example using the fetch() function:
1 import React, { useState, useEffect } from 'react'; 2 3 function MyComponent() { 4 const [data, setData] = useState(null); 5 const [isLoading, setIsLoading] = useState(true); 6 const [error, setError] = useState(null); 7 8 useEffect(() => { 9 fetch('/api/data') 10 .then(response => response.json()) 11 .then(data => { 12 setData(data); 13 setIsLoading(false); 14 }) 15 .catch(error => { 16 setError(error); 17 setIsLoading(false); 18 }); 19 }, []); 20 21 if (isLoading) return 'Loading...'; 22 if (error) return 'An error occurred: ' + error.message; 23 24 return ( 25 <div> 26 {data.map(item => ( 27 <p key={item.id}>{item.name}</p> 28 ))} 29 </div> 30 ); 31 } 32
In the example above, we're using the useState() and useEffect() hooks to fetch data from an API and store it in the state. We're also handling the loading and error states.
While the above example works, it's quite verbose and doesn't handle caching or background updates. This is where React Query comes in. With React Query, data fetching becomes a lot simpler and more powerful:
1 import { useQuery } from 'react-query'; 2 3 function MyComponent() { 4 const { isLoading, error, data } = useQuery('data', () => 5 fetch('/api/data').then(response => response.json()) 6 ); 7 8 if (isLoading) return 'Loading...'; 9 if (error) return 'An error occurred: ' + error.message; 10 11 return ( 12 <div> 13 {data.map(item => ( 14 <p key={item.id}>{item.name}</p> 15 ))} 16 </div> 17 ); 18 } 19
In the example above, we're using the useQuery() hook from React Query to fetch data. The hook handles the loading and error states for us, and it also caches the data and refetches it in the background when needed.
State management is possibly the hardest thing to get right in React applications. It involves storing, accessing, and updating the state - the data that changes over time and affects what's rendered on the screen. State can be local to a component, or it can be shared across multiple components.
Good state management can make your app fast, easy to debug, and fun to work with. Bad state management, on the other hand, can lead to bugs, performance problems, and a lot of frustration. That's why it's important to choose the right tools and patterns for managing state in your React applications.
As we've seen, Context API, Redux, and React Query are three popular tools for managing state in React. They each have their strengths and use cases:
Lazy loading is a technique where you delay the loading of data until it's needed. This can improve the performance of your React applications by reducing the amount of data that needs to be loaded upfront.
React Query provides a useQuery() hook that supports lazy loading out of the box. You can use the enabled option to control when the query runs:
1 const { data, error } = useQuery('todos', fetchTodos, { 2 enabled: false, // The query will not run automatically 3 }); 4
In the example above, the query will not run automatically when the component mounts. You can manually run the query later by calling the refetch() function returned by useQuery().
Server state memoizing is another powerful feature provided by React Query. It allows you to cache server state in memory and share it across multiple components. This can improve the performance of your React applications by reducing the number of network requests.
React Query automatically dedupes multiple requests for the same data and caches the data in memory. It also provides a useQuery() hook that supports server state memoizing:
1 const { data, error } = useQuery('todos', fetchTodos); 2
In the example above, the useQuery() hook will fetch the todos from the server and cache them in memory. If another component also uses this hook with the same query key ('todos'), React Query will return the cached data instead of making a new network request.
Asynchronous APIs are a fundamental part of modern web development. They allow you to perform long-running tasks, such as fetching data from a server, without blocking the main thread.
React Query provides several hooks for working with asynchronous APIs, such as useQuery() and useMutation(). These hooks handle the loading and error states for you, and they also support caching, background updates, and stale data refetching.
Now, you might be wondering, "How can I write all this code efficiently?" Well, this is where WiseGPT comes into play. WiseGPT is a promptless Generative AI for React developers that writes code in your style without context limit. It also provides API integration by accepting Postman collection and supports extending UI in the VSCode itself. Imagine having a tool that understands your coding style and generates accurate, efficient code snippets for you. Sounds exciting, right? Stay tuned for more on this!
Let's create a simple React app that uses the Context API to pass a theme down the component tree. We'll have a ThemeContext with a Provider that passes the current theme and a function to toggle the theme.
1 import React, { createContext, useState } from 'react'; 2 3 // Create a Context object 4 const ThemeContext = createContext(); 5 6 function ThemeProvider({ children }) { 7 const [theme, setTheme] = useState('light'); 8 const toggleTheme = () => { 9 setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light')); 10 }; 11 12 // Provide the current theme and toggleTheme function to children 13 return ( 14 <ThemeContext.Provider value={{ theme, toggleTheme }}> 15 {children} 16 </ThemeContext.Provider> 17 ); 18 } 19 20 export { ThemeContext, ThemeProvider }; 21
In the example above, we're creating a ThemeContext and a ThemeProvider component. The ThemeProvider uses the useState() hook to manage the current theme and provides a toggleTheme function to switch between 'light' and 'dark' themes.
Now, let's create a simple counter app using Redux. We'll have a counter reducer that handles 'INCREMENT' and 'DECREMENT' actions, and a Counter component that dispatches these actions.
1 import React from 'react'; 2 import { createStore } from 'redux'; 3 import { Provider, useDispatch, useSelector } from 'react-redux'; 4 5 // This is a reducer 6 function counter(state = 0, action) { 7 switch (action.type) { 8 case 'INCREMENT': 9 return state + 1; 10 case 'DECREMENT': 11 return state - 1; 12 default: 13 return state; 14 } 15 } 16 17 // Create a Redux store 18 let store = createStore(counter); 19 20 function Counter() { 21 const count = useSelector(state => state); 22 const dispatch = useDispatch(); 23 24 return ( 25 <div> 26 Count: {count} 27 <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button> 28 <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button> 29 </div> 30 ); 31 } 32 33 function App() { 34 return ( 35 <Provider store={store}> 36 <Counter /> 37 </Provider> 38 ); 39 } 40 41 export default App; 42
In the example above, we're creating a Redux store with a counter reducer. The Counter component uses the useSelector() hook to access the current count from the Redux store, and the useDispatch() hook to dispatch 'INCREMENT' and 'DECREMENT' actions.
Finally, let's create a simple app that fetches todos from an API using React Query. We'll use the useQuery() hook to fetch the todos and handle the loading, error, and success states.
1 import React from 'react'; 2 import { useQuery } from 'react-query'; 3 4 async function fetchTodos() { 5 const response = await fetch('/api/todos'); 6 if (!response.ok) { 7 throw new Error('Network response was not ok'); 8 } 9 return response.json(); 10 } 11 12 function Todos() { 13 const { isLoading, error, data: todos } = useQuery('todos', fetchTodos); 14 15 if (isLoading) return 'Loading...'; 16 if (error) return 'An error occurred: ' + error.message; 17 18 return ( 19 <ul> 20 {todos.map(todo => ( 21 <li key={todo.id}>{todo.title}</li> 22 ))} 23 </ul> 24 ); 25 } 26 27 export default Todos; 28
In the example above, we're using the useQuery() hook from React Query to fetch todos from an API. The hook handles the loading, error, and success states for us.
Choosing the right tool for managing state in your React applications depends on your specific needs and the complexity of your app.
In the end, the best tool for your project depends on your specific needs. If you're building a small app with simple state management needs, Context API might be enough. If you're building a large app with complex state management needs, Redux might be a better choice. If you're dealing with server state, React Query is a great tool.
Remember, these are just tools. The most important thing is to understand the concepts and patterns behind them. Once you understand these, you can apply them to any tool or library.
And remember, tools like WiseGPT can make your coding journey much smoother. It's like having a pair of extra hands that write code in your style, without any context limit. It's worth checking out!
That's it for this post. I hope you found it helpful. Happy coding!
Tired of manually designing screens, coding on weekends, and technical debt? Let DhiWise handle it for you!
You can build an e-commerce store, healthcare app, portfolio, blogging website, social media or admin panel right away. Use our library of 40+ pre-built free templates to create your first application using DhiWise.