Latest news about Bitcoin and all cryptocurrencies. Your daily crypto news habit.
Learning React Hooks can be a little bit intimidating because it’s a different way of working with components. Features that were previously exclusive to class based components can now be utilised with function components. With Hooks, function components can be used to manage state, make use of a component’s lifecycle events, as well as connect to the context of React apps.
As a result, our function components are going to be a little more complex than what we may be used to. That being said, not every function component you write has to make use of Hooks. I think it’s still good to have a pattern whereby we have container components that have our business logic and presentation components that have a responsibility of UI presentation.
I should say this from the onset, there are a ton articles and tutorials on understanding React Hooks, and most of the one’s I’ve come across are really good. The aim of this post is to provide developers with an alternative to the the other guidelines that are out there. I wouldn’t say this approach is better, but if you’re like me and you prefer to delve into building a small app that demonstrates as well as explains what you’re learning, then this one is for you 😉.
Some Ground Rules
Like most things in life, we can’t use these Hooks wherever and however we want — there are rules. You must only use these Hooks at the top level of your function components. So we cannot initiate/implement hooks in nested functions or code blocks like the if statement, for loops or any other form of nesting. They have to appear at the top in the root level of the function component.
Learn As We Build
To give you a more hands on feel of React Hooks, we’re going to learn some of the main or core Hooks by building a very small app with basic authentication (without a sign up step or an actual database) and the ever mundane ‘to-do list’ example 🙄. The main aim of this is to give you a practical idea of how to leverage the various Hooks in an application of your own.
Quick Setup
I’ll be using create-react-app with TypeScript for this exercise. If you’re not too sure on how to get TypeScript working with Facebook’s boilerplate, you can quickly check out another post of mine here and just work your way through the part that deals with this initial setup step and removing the boilerplate code that we won’t need.
For styling purposes I’ll be using styled-components and reactstrap, validator for input validation and uuid to generate unique id’s. You can install each of these and their respective type definition packages with the following commands:
npm i --save bootstrap reactstrap styled-components validator uuidnpm i --save-dev @types/reactstrap @types/styled-components @types/validator @types/uuid
Inside our src folder, you can setup the following folder structure:
├── components├── containers├── contexts├── custom-types└── utils
The utils folder is where our Custom Hooks, constants and helper functions will live. For now, we’re just going to add one helper function inside a file named, you guessed it 😄, Helpers.ts. This utility function will be used to imported and invoked when we want to make API requests.
/** * API Request handler * @param url - api endpoint * @param method - http method * @param bodyParams - body parameters of request */
export const apiRequest = async ( url: string, method: string, bodyParams?: { email: string; password: string }): Promise<any> => { const response = await fetch(url, { method, headers: { Accept: "application/json", "Content-Type": "application/json" }, body: bodyParams ? JSON.stringify(bodyParams) : undefined });
return await response.json();};
Inside the components folder, you can create a Styles.tsx file and add the following styled components which we’ll be using throughout the app:
import styled from "styled-components";
export const Wrapper = styled.div` height: 100vh; display: flex; align-items: center; justify-content: center; & button { background: rgba(51, 51, 255, 1) !important; }`;
export const ToDoContainer = styled.div` width: 400px;`;
export const ToDoItem = styled.div` text-align: center; font-weight: bold; cursor: pointer; text-decoration: ${(props: { complete?: boolean }) => props.complete ? "line-through" : "none"};`;
export const JokeContainer = styled.div` padding: 30px; text-align: center;`;
export const Header = styled.h4` text-transform: capitalize; letter-spacing: 1px; font-weight: bold; text-align: center;`;
export const Input = styled.input` width: 100%; border: 1px solid #f2f2f2; padding: 10px; margin-bottom: 10px;`;
Once you’re done with that, you can add a root container (RootContainer.tsx) inside the containers folder. It will be a basic function component for now, but we’ll add more logic to it later.
import * as React from "react";
/** Presentation */import { Wrapper } from "../components/Styles";
import Login from "./Login";
function RootContainer(){ return ( <Wrapper> <Login /> </Wrapper> );};
export default RootContainer;
I know what you’re thinking, “Where are the Hooks?” Well then, without wasting any more time…
Use State Hook (useState)
The useState hook allows us to add state to function components. It returns a state value and a function to update that value. We make use of it with the destructuring assignment syntax, as we will do with the other Hooks. You will notice in the example below that we can set the initial state by passing a value to the useState Hook.
const [state, setState] = useState(initialState);
Let’s go ahead and create a function component Login.tsx in the containers folder where we will manage the state values of the username, the password and loading (for when the sign in form is submitted) like so:
import * as React from "react";import { Button, Form, FormGroup, Input } from "reactstrap";
/** Utils */import { Header } from "../components/Styles";
function Login() { const [userEmail, setUserEmail] = React.useState(""); const [userPassword, setUserPassword] = React.useState(""); const [loading, setLoading] = React.useState(false);
return ( <Form onSubmit={e => { e.preventDefault(); // Auth handler }} > <Header>Sign in</Header> <br /> <FormGroup> <Input type="email" name="email" value={userEmail} placeholder="john@mail.com" onChange={e => setUserEmail(e.target.value)} /> </FormGroup> <FormGroup> <Input type="password" name="password" value={userPassword} placeholder="Password" onChange={e => setUserPassword(e.target.value)} /> </FormGroup> <Button type="submit" disabled={loading} block={true}> {loading ? "Loading..." : "Sign In"} </Button> </Form> );}
export default Login;
We’ve got our sign in form, the two input fields we need and state values that are updated each time there’s a change detected in the input fields.
Now, what happens if we encounter an error (i.e. someone tries submitting the form with an invalid username or password)? Surely we should have some logic that handles this, as well as a message to display the feedback to the user. Before we start implementing that logic in this component, error handling, as in most apps, is something we’re likely to do in more than one place. So why not create something reusable? That is a perfect segue for us to explore Custom Hooks.
Use Custom Hook (use…)
Custom hooks allow us to easily share logic across components and I think this is something many developers will grow to love ❤️, I certainly have.
You may be wondering why I opted to jump straight to custom hooks before touching on the others core Hooks. Firstly, you don’t need to worry about a steep learning curve or any major syntactical differences. Second, it’s the next logical step in the app we’re building. Third, it gives us a chance to create a custom hook that makes use of the first core React Hook that we just learned, useState.
Custom hooks are simply functions that deal with specific logic to reduce complexity and repetition. These functions allow us to utilise the core React Hooks and extract the component logic.
An important thing to remember is that the rules of React Hooks still apply to them.
Now, let’s create our first custom hook which will deal with error handling in our app. Inside the utils folder, create another folder named custom-hooks where we’ll add ErrorHandler.tsx.
import * as React from "react";
const useErrorHandler = (initialState: string | null) => { const [error, setError] = React.useState(initialState); const showError = (errorMessage: string | null) => { setError(errorMessage); window.setTimeout(() => { setError(null); }, 3000); }; return { error, showError };};
export default useErrorHandler;
What’s going on in here 🤔?
Firstly, we’re making use of the useState Hook to have a state value for an error and a function that updates that value. Next, we have a function called showError that receives an errorMessage parameter which it uses to update the error state value by utilising the setError update function. After setting the error value so the user can see, it makes use of the setTimeout function to reset the error value back to null after 3 seconds. Feel free to change the duration of how long the error message should be displayed.
All we want from this custom hook is the value of the error message and the function that handles the setting and resetting of the error message, thus we only export error and showError.
What does this look like when we import it into our Login.tsx container?
const { error, showError } = useErrorHandler(null);
Before we bring it all together, let’s add a presentation component that displays our error message in the components folder. You can name it ErrorMessage.tsx (so original). This component will have the following content:
import * as React from "react";import styled from "styled-components";
const ErrorMessage = styled.p` text-align: center; margin-top: 10px; color: #ff0000;`;
const ErrorMessageContainer: React.FC<{ errorMessage: string | null }> = ({ errorMessage}) => { return <ErrorMessage>{errorMessage}</ErrorMessage>;};
export default ErrorMessageContainer;
We also want to add functionality for validating the username (email) and password that a user submits on the sign in form, so let’s include another function in our Helpers.ts file called validateLoginForm.
import * as validator from "validator";
/** Handle form validation for the login form * @param email - user's auth email * @param password - user's auth password * @param setError - function that handles updating error state value */export const validateLoginForm = ( email: string, password: string, setError: (error: string | null) => void): boolean => { // Check for undefined or empty input fields if (!email || !password) { setError("Please enter a valid email and password."); return false; }
// Validate email if (!validator.isEmail(email)) { setError("Please enter a valid email address."); return false; }
return true;};
We don’t have a backend to check if a user exists in any real database, so we’ll simply send a POST request with a username and password to a fake REST API each time a user clicks on Sign In. The fake REST API we’ll use in this case is JSONPlaceholder.
Let’s bring it all together now…
import * as React from "react";import { Button, Form, FormGroup, Input } from "reactstrap";
/** Presentation */import ErrorMessage from "../components/ErrorMessage";
/** Custom Hooks */import useErrorHandler from "../utils/custom-hooks/ErrorHandler";
/** Utils */import { apiRequest, validateLoginForm } from "../utils/Helpers";import { Header } from "../components/Styles";
function Login() { const [userEmail, setUserEmail] = React.useState(""); const [userPassword, setUserPassword] = React.useState(""); const [loading, setLoading] = React.useState(false); const { error, showError } = useErrorHandler(null);
const authHandler = async () => { try { setLoading(true); const userData = await apiRequest( "https://jsonplaceholder.typicode.com/users", "post", { email: userEmail, password: userPassword } ); const { id, email } = userData; } catch (err) { setLoading(false); showError(err.message); } };
return ( <Form onSubmit={e => { e.preventDefault(); if (validateLoginForm(userEmail, userPassword, showError)) { authHandler(); } }} > <Header>Sign in</Header> <br /> <FormGroup> <Input type="email" name="email" value={userEmail} placeholder="john@mail.com" onChange={e => setUserEmail(e.target.value)} /> </FormGroup> <FormGroup> <Input type="password" name="password" value={userPassword} placeholder="Password" onChange={e => setUserPassword(e.target.value)} /> </FormGroup> <Button type="submit" disabled={loading} block={true}> {loading ? "Loading..." : "Sign In"} </Button> <br /> {error && <ErrorMessage errorMessage={error} />} </Form> );}
export default Login;
Now that we have been “authenticated”, what do we do with the id and email that our fake REST API returns? Also, how do we make this authenticated status available to the rest of our app? Time to dive into the useContext Hook.
Use Context Hook (useContext)
The Context API allows us to pass and access global state around our component tree without having to pass down as props all the time. Any component nested inside the Context provider can tap into the context and retrieve the state values, as well as references to functions that can act on the state.
We can call the useContext hook to get access to the context. However, as you may know, we can have different contexts in our app, and so we’ll need a way of identifying the context we want to tap into and make use of. To do so, import the context and pass it as a parameter to the useContext function like below:
import { authContext } from "../contexts/AuthContext";
const auth = React.useContext(authContext);
For our app, I’m going to set my context to be a JavaScript object which holds the current authentication status and also hold the references to functions that enable us to change the authentication status.
First off, I’m going to create a static type for the user authentication status and put this type in a index.ts file in the custom-types folder. The UserAuth type will have an id and email property for the user who is signed in.
export type UserAuth = { id: number; email: string;};
I also want to have a default status for an unauthenticated user, and it will live inside the Consts.ts file of the utils folder.
export const DEFAULT_USER_AUTH = { id: 0, email: "" };
Before we get to applying useContext, we are going to create another custom hook that will contain the logic for setting and resetting the authentication status. I’m going to call this file AuthHandler.tsx and save it in the custom-hooks folder.
import * as React from "react";
/** Custom types */import { UserAuth } from "../../custom-types";
/** Utils */import { DEFAULT_USER_AUTH } from "../Consts";
const useAuthHandler = (initialState: UserAuth) => { const [auth, setAuth] = React.useState(initialState);
const setAuthStatus = (userAuth: UserAuth) => { window.localStorage.setItem("UserAuth", JSON.stringify(userAuth)); setAuth(userAuth); };
const setUnauthStatus = () => { window.localStorage.clear(); setAuth(DEFAULT_USER_AUTH); };
return { auth, setAuthStatus, setUnauthStatus };};
export default useAuthHandler;
You may have noticed that we are not only updating the value of the auth status in our application’s state, but also in our local storage. The reason for this, if you haven’t already guess it, is that we don’t want to lose the value of our auth state when the browser is refreshed. So I’m going to go ahead and create another helper function to add to the Helpers.ts file that retrieves the user’s auth value from local storage.
/** Return user auth from local storage value */export const getStoredUserAuth = (): UserAuth => { const auth = window.localStorage.getItem("UserAuth"); if (auth) { return JSON.parse(auth); } return DEFAULT_USER_AUTH;};
Now that we’ve got that out of the way, let’s create our auth context and export its provider! You can add the AuthContext.tsx file in the contexts folder.
import * as React from "react";
/** Custom types */import { UserAuth } from "../custom-types";/** Custom Hooks */import useAuthHandler from "../utils/custom-hooks/AuthHandler";/** Utils */import { DEFAULT_USER_AUTH } from "../utils/Consts";import { getStoredUserAuth } from "../utils/Helpers";
interface IAuthContextInterface { auth: UserAuth; setAuthStatus: (userAuth: UserAuth) => void; setUnauthStatus: () => void;}
export const authContext = React.createContext<IAuthContextInterface>({ auth: DEFAULT_USER_AUTH, setAuthStatus: () => {}, setUnauthStatus: () => {}});
const { Provider } = authContext;
const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children}) => { const { auth, setAuthStatus, setUnauthStatus } = useAuthHandler( getStoredUserAuth() );
return ( <Provider value={{ auth, setAuthStatus, setUnauthStatus }}> {children} </Provider> );};
export default AuthProvider;
Once we’ve done that, there are two things we need to do:
- Wrap our app component in the AuthProvider from our auth context.
- Tap into the auth context and set the user’s authentication status in the Login component once they’re signed in.
App.tsx
import * as React from "react";
import RootContainer from "./containers/RootContainer";
/** Context API */import AuthContextProvider from "./contexts/AuthContext";
function App() { return ( <AuthContextProvider> <RootContainer /> </AuthContextProvider> );}
export default App;
Login.tsx
import * as React from "react";import { Button, Form, FormGroup, Input } from "reactstrap";
/** Presentation */import ErrorMessage from "../components/ErrorMessage";
/** Custom Hooks */import useErrorHandler from "../utils/custom-hooks/ErrorHandler";
/** Context */import { authContext } from "../contexts/AuthContext";
/** Utils */import { apiRequest, validateLoginForm } from "../utils/Helpers";import { Header } from "../components/Styles";
function Login() { const [userEmail, setUserEmail] = React.useState(""); const [userPassword, setUserPassword] = React.useState(""); const [loading, setLoading] = React.useState(false); const auth = React.useContext(authContext); const { error, showError } = useErrorHandler(null);
const authHandler = async () => { try { setLoading(true); const userData = await apiRequest( "https://jsonplaceholder.typicode.com/users", "post", { email: userEmail, password: userPassword } ); const { id, email } = userData; auth.setAuthStatus({ id, email }); } catch (err) { setLoading(false); showError(err.message); } };
return ( <Form onSubmit={e => { e.preventDefault(); if (validateLoginForm(userEmail, userPassword, showError)) { authHandler(); } }} > <Header>Sign in</Header> <br /> <FormGroup> <Input type="email" name="email" value={userEmail} placeholder="john@mail.com" onChange={e => setUserEmail(e.target.value)} /> </FormGroup> <FormGroup> <Input type="password" name="password" value={userPassword} placeholder="Password" onChange={e => setUserPassword(e.target.value)} /> </FormGroup> <Button type="submit" disabled={loading} block={true}> {loading ? "Loading..." : "Sign In"} </Button> <br /> {error && <ErrorMessage errorMessage={error} />} </Form> );}
export default Login;
If you take another look at the screenshot of the UI for our To Do app, you’ll notice a Chuck Norris joke right under the heading ‘My To Do List’. We want to make sure we start our days off with a smile before writing out what have to get done, so let’s make sure that a random joke shows up right above our form. To build this in, we’re going to use the useEffect hook.
Use Effect Hook (useEffect)
The useEffect hook takes two arguments, the first is a function that executes (or gets called) after every render. This function will run when the component mounts, as well as when it updates. Also, if you want the same component clean up functionality that componentWillUnmount gives us, you can return a function from inside a useEffect hook, and it will use that function to clean up.
What about the second argument? You can optionally provide an array of inputs. If so, the effect will only run after renders where those inputs have changed. Something important to note, is that if you do not provide this second argument, the function (or the effect) in our first argument will run after every render of the component. The other option with this second argument is to provide an empty array, which means to only run on mount and unmount.
const [todos, updateTodos] = useState([])
// Only execute on mountuseEffect(() => { window.localStorage.setItem(‘todos’, JSON.stringify(todos))}, [])
// Execute when there’s been a change in our todos list (componentDidUpdate):useEffect(() => { window.localStorage.setItem(‘todos’, JSON.stringify(todos))}, [todos])
// Execute clean up function on unmountuseEffect(() => { return () => { console.log(‘Clean up function’) }}, [])
Alright, let’s create a component called RandomJoke.tsx in the containers folder and call a random joke from Random Geek Jokes REST API that will display above our to do list. We only want the request to be made once when the component mounts, so we’re going to pass an empty array for the second argument for our useEffect hook.
import * as React from "react";
/** Presentation/UI */import { JokeContainer } from "../components/Styles";/** Utils */import { apiRequest } from "../utils/Helpers";
const RandomJoke: React.FC<{}> = () => { const [joke, setJoke] = React.useState(""); const [loading, setLoading] = React.useState(false);
React.useEffect(() => { const getRandomJoke = async () => { setLoading(true); const joke = await apiRequest( "https://geek-jokes.sameerkumar.website/api", "get" ); setLoading(false); setJoke(joke); }; getRandomJoke(); }, []);
return ( <JokeContainer> {loading ? "Why so serious, let's put a smile on your face :)" : joke} </JokeContainer> );};
export default RandomJoke;
That should get us laughing, but maybe not as hard as this guy…
Let’s move on to create the form that we’ll use to populate our to do list. For this next part, we’ll be using the useRef hook.
Use Ref Hook (useRef)
When we work with class based components, we could use references to interact with the DOM elements on our page. Before hooks, this is something we couldn’t do in function components because they have no properties that we could use to store the references.
Go ahead and create a function component in a file called AddToDo.tsx. We’re going to create a reference by storing it in a constant called textInput. You can optionally set an initial value for this reference, I’m going to set it as null. The reference to the input field is stored in the constant once we add textInput to the ref property in an input JSX element.
const textInput = React.useRef<HTMLInputElement>(null);
We’re going to be using the internal state management of the input element and use a ref to extract its current value whenever we need it.
To get the value of the input element, you can use the textInput which returns an object with a property that refers to the relevant HTML element. Like so:
const toDo = textInput.current.value;
The current property holds the actual HTML element reference. On current, you can then access value because it points to the input element and input elements in JavaScript have a value field, which is their current value.
Our component to add a new ‘to-do’ item will look like this:
import * as React from "react";import { Button, Form } from "reactstrap";
/** Presentation */import ErrorMessage from "../components/ErrorMessage";import { Input } from "../components/Styles";/** Custom Hooks */import useErrorHandler from "../utils/custom-hooks/ErrorHandler";
const AddToDo: React.FC<{}> = () => { const { error, showError } = useErrorHandler(null); const textInput = React.useRef<HTMLInputElement>(null);
const addNewToDoItem = () => { if (textInput.current) { const toDo = textInput.current.value; console.log('Today I want to:', toDo); } else { showError("Please type an item before clicking add."); } };
return ( <Form onSubmit={e => { e.preventDefault(); addNewToDoItem(); }} > <Input type="text" ref={textInput} placeholder="Add to do item" /> <Button type="submit" block={true}> Add </Button> <br /> {error && <ErrorMessage errorMessage={error} />} </Form> );};
export default AddToDo;
Now that we have that done, we want to be able to store, fetch and update the status of each item in our to-do list, and not just log them individually in the browser console. To accomplish this we’ll use a new context and the useReducer hook.
Use Reducer Hook (useReducer)
If you’re familiar with Redux, then you probably know what a reducer is. If this is the first time you’re hearing about reducers, not to worry.
A reducer is a function that helps us to manage application state. It is a pure function in the sense that it always returns the same output, given a certain input. In the case of a reducer function, it takes two arguments or parameters, the current state and an action.
(state, action) => newState
The action is an object with a type property, and based on the action type, the reducer performs a certain state transition:
const counterReducer = (count, action) => { if (action.type === 'INCREASE') { return count + 1; }
if (action.type === 'DECREASE') { return count - 1; }
return count;};
After we’ve created our reducer, we need to register it and use it correctly. To do that we pass it as a parameter to the useReducer hook. We can pass a second parameter to the useReducer hook which will be our initial state. We then use the same destructuring syntax we used with useState.
const [count, dispatch] = useReducer(counterReducer, [])
Time to implement it in our app.
Let’s create some static types in the index.ts file in our custom-types folder.
export type ToDoItemType = { id: string; toDo?: string; complete?: boolean };
export enum ActionType { add = "ADD", delete = "DELETE", updateStatus = "UPDATE"}
Next up, let’s create a to-do context called ToDoContext.tsx in the contexts folder. This file will contain our reducer that handles the actions for managing the state of the to-do list, as well as the context that will allow us to access the state value and a reference to the function for updating the application’s state.
Our reducer will handle adding of items to our to-do list array:
case ActionType.add: return { toDoList: [...state.toDoList, action.payload] };
It will handle updating the completion status of an item in the to-do list array:
case ActionType.updateStatus: return { toDoList: state.toDoList.map(toDo => { if (toDo.id === action.payload.id) { return { ...toDo, complete: !toDo.complete }; } return toDo; }) };
Lastly, it will handle deleting an item from the to-do list array:
case ActionType.delete: return { toDoList: state.toDoList.filter(toDo => toDo.id !== action.payload.id) };
ToDoContext.tsx
import * as React from "react";
/** Custom types */import { ActionType } from "../custom-types";
interface IState { toDoList: Array<{ id: string; toDo?: string; complete?: boolean }>;}
interface IAction { type: ActionType; payload: { id: string; toDo?: string; complete?: boolean; };}
interface ItoDoContextInterface { state: { toDoList: Array<{ id: string; toDo?: string; complete?: boolean }>; }; updateToDoList: React.Dispatch<IAction>;}
const initialState: IState = { toDoList: [] };
const reducer: React.Reducer<IState, IAction> = (state, action) => { switch (action.type) { case ActionType.add: return { toDoList: [...state.toDoList, action.payload] }; case ActionType.updateStatus: return { toDoList: state.toDoList.map(toDo => { if (toDo.id === action.payload.id) { return { ...toDo, complete: !toDo.complete }; } return toDo; }) }; case ActionType.delete: return { toDoList: state.toDoList.filter(toDo => toDo.id !== action.payload.id) }; default: throw new Error(); }};
export const toDoContext = React.createContext<ItoDoContextInterface>({ state: { toDoList: [] }, updateToDoList: () => {}});
const { Provider } = toDoContext;
const ToDoProvider: React.FC<{ children: React.ReactNode }> = ({ children}) => { const [toDoList, updateToDoList] = React.useReducer(reducer, initialState);
return ( <Provider value={{ state: toDoList, updateToDoList }}> {children} </Provider> );};
export default ToDoProvider;
If you’re sitting feeling like it’s a bit of an overload, don’t stress because the hardest part is over 💪, we’re almost done.
Let’s update our AddToDo (AddToDo.tsx) component so that it taps into the toDoContext that we just created.
import * as React from "react";import { Button, Form } from "reactstrap";import uuid from "uuid";
/** Context */import { toDoContext } from "../contexts/ToDoContext";/** Presentation */import ErrorMessage from "../components/ErrorMessage";import { Input } from "../components/Styles";/** Custom Hooks */import useErrorHandler from "../utils/custom-hooks/ErrorHandler";/** Utils */import { ActionType } from "../custom-types";
const AddToDo: React.FC<{}> = () => { const { updateToDoList } = React.useContext(toDoContext); const { error, showError } = useErrorHandler(null); const textInput = React.useRef<HTMLInputElement>(null);
const addNewToDoItem = () => { if (textInput.current) { const toDo = textInput.current.value; updateToDoList({ type: ActionType.add, payload: { id: uuid(), toDo } }); textInput.current.value = ""; } else { showError("Please type an item before clicking add."); } };
return ( <Form onSubmit={e => { e.preventDefault(); addNewToDoItem(); }} > <Input type="text" ref={textInput} placeholder="Add to do item" /> <Button type="submit" block={true}> Add </Button> <br /> {error && <ErrorMessage errorMessage={error} />} </Form> );};
export default AddToDo;
As I mentioned before, we don’t want to simply log the items in our to do list, so let’s create a component in the containers folder that will loop present our items and allow us to click on them individually to toggle their completion status.
ToDoList.tsx
import * as React from "react";
/** Context */import { toDoContext } from "../contexts/ToDoContext";/** Styles */import { ToDoItem } from "../components/Styles";/** Utils */import { ActionType, ToDoItemType } from "../custom-types";
const ToDoList: React.FC<{}> = () => { const { state, updateToDoList } = React.useContext(toDoContext);
return ( <React.Fragment> {state.toDoList.map(({ id, toDo, complete }: ToDoItemType, i: number) => { return ( <ToDoItem key={id} onClick={() => updateToDoList({ type: ActionType.updateStatus, payload: { id } }) } complete={complete} > {i + 1}. {toDo} </ToDoItem> ); })} </React.Fragment> );};
export default ToDoList;
Now we need a component that will wrap the following components as children: RandomJoke.tsx, AddToDo.tsx and ToDoList.tsx. We’ll create a function component and call this parent component ToDo.tsx.
import * as React from "react";
/** Styles */import { Header } from "../components/Styles";
/** Components */import AddToDo from "./AddToDo";import RandomJoke from "./RandomJoke";import ToDoList from "./ToDoList";/** Presentation/UI */import { ToDoContainer } from "../components/Styles";
function ToDo() { return ( <ToDoContainer> <Header>My to do list</Header> <RandomJoke /> <AddToDo /> <ToDoList /> </ToDoContainer> );}
export default ToDo;
Finally, we want our RootContainer.tsx component to tap into the auth context so that when a user is authenticated our application will render the to-do list (ToDo.tsx), and if the local storage is cleared, the user will be signed out and the sign-in (Login.tsx) will be rendered to the screen.
import * as React from "react";
/** Context */import { authContext } from "../contexts/AuthContext";/** Presentation */import { Wrapper } from "../components/Styles";
import Login from "./Login";import ToDo from "./ToDo";
function RootContainer() { const { auth } = React.useContext(authContext); return ( <Wrapper> {auth.id ? <ToDo /> : null} {!auth.id && <Login />} </Wrapper> );}
export default RootContainer;
Now it’s time for the moment you’ve been waiting and coding for 🙌
Go ahead and run npm start
There are a few additional React hooks that we didn’t look at in this tutorial, namely useCallback, useMemo, useImperativeHandle, useLayoutEffect and useDebugValue. Go ahead and explore them and see how you can utilise them in your React applications.
You can find the source code for this application here.
Learn React Hooks By Building An Auth Based To Do App was originally published in Hacker Noon on Medium, where people are continuing the conversation by highlighting and responding to this story.
Disclaimer
The views and opinions expressed in this article are solely those of the authors and do not reflect the views of Bitcoin Insider. Every investment and trading move involves risk - this is especially true for cryptocurrencies given their volatility. We strongly advise our readers to conduct their own research when making a decision.