Disclaimer: Only for ReactJS/JS Devs - TLDR
Hey NERDS. We are gonna discuss about some AWESOME concepts in React like
- Hooks (useState, useEffect, useContext & useReducer) &
- Context API
FIRST THINGS FIRST,
This blog assumes that if not decent, at-least some level of understanding in when & how to use class components, state, props & handling events.
With that clarification out of our way, lets get started ->
For those who use Class Components for a while, as per docs HOOKS are a new addition in React 16.8 & by this FEB 2021, it will be 2 yrs old (not so new, may be)
Having said that, Hooks are better when it comes to performance, abstraction & readability in comparison to class based components. Yes true, if you don't believe my word, take it from React Core Team demo
NOTE: Video is about inspiration for HOOKS
But ... What is HOOK?
Provides some direct API that makes us to use state in functional components . In simple terms, they let you use state & other react features without having to write a class.
Here is an implementation of hook called useState
hook for example,
import React, { Fragment, useState } from "react";
export const Example = () => {
const [name, setName] = useState("");
const [login, setLogin] = useState(false);
return (
<Fragment>
{!login ? (
<input
type="name"
value={name}
placeholder="Enter your Name"
onChange={(e) => setName(e.target.value)}
/>
) : (
<div className="display__text">Hello {name}</div>
)}
<button type="submit" onClick={() => setLogin(!login)}>
{!login ? "Login" : "Go back"}
</button>
</Fragment>
);
};
From above, you can see it is just simple login simulation app, i have used useState
hook to use the state within the functional Component, one state for handling user input name field and other for toggling into welcome page.
Yes, you can useState
as many times you want, there is no restriction but there is one caveat to using hooks in general, that is they should be used only inside the function definition.
Just for Comparison, check the equivalent class component implementation here,
import React, { Component, Fragment } from "react";
export class Example extends Component {
constructor(props) {
super(props);
this.state = {
name: "",
login: false
};
}
render() {
return (
<Fragment>
{!this.state.login ? (
<input
type="name"
value={this.state.name}
placeholder="Enter your Name"
onChange={(e) => this.setState({ name: e.target.value })}
/>
) : (
<div className="display__text">Hello {this.state.name}</div>
)}
<button
type="submit"
onClick={() => this.setState({ login: !this.state.login })}
>
{!this.state.login ? "Login" : "Go back"}
</button>
</Fragment>
);
}
}
I bet you can already see the difference, not only 10 unnecessary lines are removed, overall code logic looks neat. We can further drill down in our hooks implementation
import React, { Fragment, useState } from "react";
export const Example = () => {
//using objects to keep multiple states
const [state, setState] = useState({ name: "", login: false });
const { name, login } = state;
return (
<Fragment>
{!login ? (
<input
type="name"
value={name}
placeholder="Enter your Name"
onChange={(e) => setState({ ...state, name: e.target.value })}
/>
) : (
<div className="display__text">Hello {name}</div>
)}
<button
type="submit"
onClick={() => setState({ ...state, login: !login })}
>
{!login ? "Login" : "Go back"}
</button>
</Fragment>
);
};
Modified useState implementation
Well, this look more better than before. Instead of using many useState
(s) , irrespective of number of states in your application, we can use single useState
call with an object to account for all the states. Also, check i have also changed the setLogin, setName into single setState call with (...)
spread operator to update the respective states on particular event.
All is fine. But how to use life cycle methods like componentDidMount
, componentDidUpdate
, componentWillUnmount
for API calls or side effects in HOOKS?
To answer that, we have an another hook called useEffect
which takes function as a argument. Cool thing about this hook is, it replaces all the life cycle methods mentioned above.
import React, { Fragment, useState, useEffect } from "react";
import Axios from "axios";
export const Example = () => {
const [state, setState] = useState({ posts: [] });
const postsUrl = "https://jsonplaceholder.typicode.com/posts";
useEffect(() => {
//Fetching Posts
const fetchPosts = async () => {
const posts = await Axios.get(postsUrl);
setState({ ...state, posts: posts.data });
};
try {
fetchPosts();
} catch (err) {
console.log("Network error", err);
}
//dependency array is none, it acts as componentDidMount
}, []);
return (
<>
<h1>My posts</h1>
<ul>
{state.posts.length > 0 &&
state.posts.map((posts, index) => {
return <li key={index}>{posts.title}</li>;
})}
</ul>
</>
);
};
Basic useEffect implementation (componentDidMount)
import React, { useState, useEffect } from "react";
export const Example = () => {
const [title, setTitle] = useState("");
useEffect(() => {
document.title = title;
//runs everytime when title state is changed
//Acts as componentDidUpdate
}, [title]);
return (
<input
type="text"
name="title"
value={title}
placeholder="Enter your title"
onChange={(e) => setTitle(e.target.value)}
/>
);
};
Basic useEffect implementation (componentDidUpdate)
From above you can see componentDidMount
& componentDidUpdate
implementation in hooks with useEffect
. First one fetches posts from an fake rest API endpoint and empty dependency array is passed as an second argument to simulate componentDidMount
and second one updates the title tab of browser whenever the input field value is updated by the user. Note it has an dependency array value and it simulates componentDidUpdate
.
In addition to useState
& useEffect
hook there are built_in_hooks like useReducer
, useRef
, useLayoutEffect
& some more which you can check in React Docs
Alright! What is Context in nutshell?
In simple terms, if you use context, we can avoid prop drilling meaning there is no need to pass props to unnecessary intermediate component in between when you work with deep nested components.
import React, { createContext } from "react";
//creating context
export const AppContext = createContext();
//Not necessary in react new versions
export const AppConsumer = AppContext.Consumer;
//Exporting provider
export const AppProvider = ({ children }) => {
return <AppContext.Provider>{children}</AppContext.Provider>;
};
useReducer WITH contextAPI (AppContext.js)
You can see, I have to createcontext
first and passing initial values to it is optional, then export respective context
and provider
in AppContext.js.
Note: You have to wrap the children
prop inside the Provider
pass in the values you wanna pass in value
prop in Provider
so that you can use the values latter in wherever you wanna use in component tree (Here we have passed dispatch function and state)
import React from "react";
import "./styles.css";
import { Example } from "./Components/Example";
import { AppProvider } from "./Contexts/AppContext";
export default function App() {
return (
<AppProvider>
<Example />
</AppProvider>
);
}
useReducer WITH contextAPI(App.js)
Make sure to wrap the provider with components you wanna use context values in App.js
as well.
So, all set before talking about how to use context values inside the respective components, there is another hook we need to discuss about which simulates how redux
works called useReducer
For those who doesn't know how redux
works, this is the simple flow diagram that explains well
Redux is quite complex to understand but in simple terms, we as users dispatch an action (trigger an event) in an application, there is pure function called reducer
which takes in respective action and the current state, updates with new state based on the action.
import React, { createContext, useReducer } from "react";
//importing Reducer
import { reducer } from "../reducer";
//creating context
export const AppContext = createContext();
//Not necessary in react new versions
export const AppConsumer = AppContext.Consumer;
//Exporting provider
export const AppProvider = ({ children }) => {
//usng useReducer hook and passing reducer and initial state to it
const [state, dispatch] = useReducer(reducer, []);
//pass the app state and dispatch function to the value prop in provider
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
};
useReducer WITH contextAPI (AppContext.js)
useReducer
hook takes in two things - reducer function and the state and return two things again the state and dispatch function
//reducer function which takes in dispatched action and update
//the respective state
export const reducer = (todos, action) => {
switch (action.type) {
case "ADD_TODO":
return [
...todos,
{
id: Date.now(),
todoName: action.payload.todoName,
completed: false
}
];
default:
return todos;
}
};
useReducer WITH contextAPI (reducer.js)
Here you can see the implementation of reducer, which is nothing but a pure function takes in state and action. Based on the action type, we update the state.
Note: payload (optional) is the data we pass while dispatching an action.
So, todos is our initial State and with help of reducer we are updating the state, here it is adding to the todo array with condition of action type and payload.
All good. Now there is only one piece remaining to fix this puzzle which is attaching dispatching function to form submission. To do that, if you remember we have passed state
and dispatch
in Provider
. In order to access that, we have an another hook called useContext
that takes in a parameter of respective provider. Just by using this one line, we will get access to context values.
import React, { Fragment, useState, useContext } from "react";
import { AppContext } from "../Contexts/AppContext";
import { Todo } from "./Todo";
export const Example = () => {
const [todoName, setTodoName] = useState("");
// here used useContext hook to access state & dispatch from top level
const { state, dispatch } = useContext(AppContext);
const handleSubmit = (e) => {
e.preventDefault();
//dispatching action here along with payload
dispatch({ type: "ADD_TODO", payload: { todoName: todoName } });
setTodoName("");
};
return (
<Fragment>
<form onSubmit={handleSubmit}>
<input
type="text"
name="todoName"
value={todoName}
onChange={(e) => setTodoName(e.target.value)}
placeholder="Enter the todo Name"
/>
<button type="submit">Submit</button>
<Todo todos={state} />
</form>
</Fragment>
);
};
useReducer WITH contextAPI (Example.js)
Now that we have our dispatch we want to attach it with form submission and pass the action type and payload along with it
Finally pass the state you get from useContext to Todo Component to list down below the field.
This might lead to one question, why so much code just to update state i can do this easily with useState
. Yes you can. But what if you want to update the todo, delete the todo, mark as completed then there will be whole lot of complexity if you use useState
that is where useReducer
comes handy.
useReducer
VS useState
useReducer
is meant for handling complex state update updates, every action for the application will be mentioned only in reducer function that makes state changes predictable. Though both does the same things, using one over another completely depends on the situation you are in.
Final Thoughts:
This is not ALL, there are other hooks which has good use cases but it is not covered here. Checkout if you want to learn more about HOOKS in this docs. Once you feel confident in using hooks, you can learn to develop your own CUSTOM HOOKS *Yes you can do that.
Please let me know if there is anything i've missed,
Happy Coding*
Top comments (0)