Building a CRUD App in React with Hooks and Axios
As a web developer, one of the most fundamental patterns you‘ll encounter is CRUD – Create, Read, Update, and Delete. Nearly every application needs to create and store some kind of data, display it to users, allow for updating it, and support removing it. While the underlying implementation may vary, the core operations remain the same.
In this guide, we‘ll walk through building a simple but fully-functional CRUD application using the popular React library along with hooks for state management and Axios for making API calls to a backend server. By the end, you‘ll have a solid understanding of how the pieces fit together and be able to apply these concepts in your own projects.
What is CRUD?
CRUD stands for the four basic types of functionality for persistent storage, especially databases:
- Create – add new entries
- Read – fetch and display existing entries
- Update – modify existing entries
- Delete – remove entries
For example, in a blog application, you might have the ability to create new posts, display a list of existing posts, edit the title or content of a post, and delete old posts you no longer want. While there are many other operations an app might support, CRUD tends to make up the backbone.
Why Use React, Hooks, and Axios?
React has become one of the most popular JavaScript libraries for building interactive UIs. Its component-based architecture and declarative approach help break complex interfaces down into reusable pieces. Hooks, added in React 16.8, allow for cleaner code and more easily reusable stateful logic compared to class components.
Axios is a lightweight HTTP client that works in the browser and Node.js. It provides an easy-to-use API and has some nice features like interceptors, request/response transformation, and error handling.
By leveraging these technologies together, we can create a robust and maintainable CRUD application with a clean separation of concerns between the frontend and backend. React will handle displaying and updating the UI, hooks will manage the internal component state, and Axios will take care of sending requests to a RESTful API.
Setting Up the Project
Let‘s get started by scaffolding a new React project. The quickest way is with Create React App:
npx create-react-app react-crud-tutorial
cd react-crud-tutorial
Next, we‘ll install the dependencies we need:
npm install axios react-router-dom
Axios will handle our API requests and react-router-dom will allow us to set up routing between different pages in our app.
We‘ll also be using React Bootstrap for some pre-built UI components to save time styling. Install it with:
npm install react-bootstrap bootstrap
Make sure to import the CSS file in your src/index.js
:
import ‘bootstrap/dist/css/bootstrap.min.css‘;
Creating the Components
Inside the src
directory, create a new folder called components
. Here we‘ll put the individual pieces that make up our CRUD interface. Let‘s start with a basic component for displaying a list of items which we‘ll call ItemList
.
// src/components/ItemList.js
import React, { useState, useEffect } from ‘react‘;
import axios from ‘axios‘;
import ListGroup from ‘react-bootstrap/ListGroup‘;
const ItemList = () => {
const [items, setItems] = useState([])
useEffect(() => {
const fetchItems = async () => {
const result = await axios(
‘https://jsonplaceholder.typicode.com/posts‘,
);
setItems(result.data);
};
fetchItems();
}, []);
return (
<ListGroup>
{items.map(item => (
<ListGroup.Item key={item.id}>{item.title}</ListGroup.Item>
))}
</ListGroup>
)
}
export default ItemList
There are a few key things happening here:
-
We import the
useState
anduseEffect
hooks from React.useState
allows us to add state to functional components anduseEffect
lets us perform side effects like fetching data. -
In the component, we declare a state variable called
items
along with a function to update it calledsetItems
. The initial value is an empty array. -
The
useEffect
hook is used to fetch data from an API when the component mounts. We‘ve defined an asynchronous function inside it calledfetchItems
that uses Axios to send a GET request tohttps://jsonplaceholder.typicode.com/posts
. This is a fake API for testing and prototyping. The response data is then put into the component state usingsetItems
. -
In the JSX being returned, we map over the
items
array and render aListGroup.Item
for each one displaying the post title. Thekey
prop is important for React to efficiently update the list.
With this setup, ItemList
will fetch posts from the API and display their titles in a list when mounted.
Next, let‘s create components for the other CRUD operations, starting with ItemCreate
for adding new items.
// src/components/ItemCreate.js
import React, { useState } from ‘react‘;
import axios from ‘axios‘;
import Form from ‘react-bootstrap/Form‘;
import Button from ‘react-bootstrap/Button‘;
const ItemCreate = () => {
const [title, setTitle] = useState(‘‘);
const [body, setBody] = useState(‘‘);
const handleSubmit = async e => {
e.preventDefault();
const result = await axios.post(
‘https://jsonplaceholder.typicode.com/posts‘,
{
title,
body,
}
);
console.log(result.data);
setTitle(‘‘);
setBody(‘‘);
}
return (
<Form onSubmit={handleSubmit}>
<Form.Group controlId="formTitle">
<Form.Label>Title</Form.Label>
<Form.Control
type="text"
placeholder="Enter title"
value={title}
onChange={e => setTitle(e.target.value)}
/>
</Form.Group>
<Form.Group controlId="formBody">
<Form.Label>Body</Form.Label>
<Form.Control
as="textarea"
placeholder="Enter body"
rows={3}
value={body}
onChange={e => setBody(e.target.value)}
/>
</Form.Group>
<Button variant="primary" type="submit">
Submit
</Button>
</Form>
);
}
export default ItemCreate;
The ItemCreate
component renders a form with fields for entering a post title and body. When submitted, it sends a POST request to the API to create a new post with the entered data. We use the useState
hook to manage the form state and the axios.post
method to send the request. Upon a successful response, we log the result and clear out the form.
The process is very similar for ItemUpdate
:
// src/components/ItemUpdate.js
import React, { useState, useEffect } from ‘react‘;
import axios from ‘axios‘;
import Form from ‘react-bootstrap/Form‘;
import Button from ‘react-bootstrap/Button‘;
const ItemUpdate = ({ match }) => {
const [title, setTitle] = useState(‘‘);
const [body, setBody] = useState(‘‘);
useEffect(() => {
const fetchItem = async () => {
const result = await axios(
`https://jsonplaceholder.typicode.com/posts/${match.params.id}`
);
setTitle(result.data.title);
setBody(result.data.body);
};
fetchItem();
}, [match]);
const handleSubmit = async e => {
e.preventDefault();
const result = await axios.put(
`https://jsonplaceholder.typicode.com/posts/${match.params.id}`,
{
title,
body,
}
);
console.log(result.data);
};
return (
<Form onSubmit={handleSubmit}>
<Form.Group controlId="formTitle">
<Form.Label>Title</Form.Label>
<Form.Control
type="text"
placeholder="Enter title"
value={title}
onChange={e => setTitle(e.target.value)}
/>
</Form.Group>
<Form.Group controlId="formBody">
<Form.Label>Body</Form.Label>
<Form.Control
as="textarea"
placeholder="Enter body"
rows={3}
value={body}
onChange={e => setBody(e.target.value)}
/>
</Form.Group>
<Button variant="primary" type="submit">
Update
</Button>
</Form>
);
};
export default ItemUpdate;
The main difference is that ItemUpdate
needs to fetch the existing post data and populate the form fields with it. We do this in a useEffect
hook that runs when the component mounts and whenever the post ID changes (e.g. if the user navigated directly to the update URL).
We get the ID from the URL parameter which is passed in via the match
prop by React Router. Inside the useEffect
, we make a GET request for the individual post data and use the response to set the title
and body
state values.
The form submission handler sends a PUT request to update the post with the new field values. Again, we‘re just logging the response here, but in a real app you would likely show some kind of success message and navigate the user back to the post list.
Finally, for deleting we don‘t need a separate component, we can add a handler to ItemList
:
// src/components/ItemList.js
// ...
const handleDelete = async id => {
await axios.delete(`https://jsonplaceholder.typicode.com/posts/${id}`);
const updatedItems = items.filter(item => item.id !== id);
setItems(updatedItems);
}
return (
<ListGroup>
{items.map(item => (
<ListGroup.Item key={item.id}>
{item.title}
<Button
variant="danger"
size="sm"
onClick={() => handleDelete(item.id)}
>
Delete
</Button>
</ListGroup.Item>
))}
</ListGroup>
)
// ...
Each list item now has a button that will delete that post when clicked. The handleDelete
function sends a DELETE request to the API and then removes the deleted item from the items
state array.
Adding Routing
We‘ve got components for each CRUD operation, but we need a way to navigate between them. React Router makes this easy. First, we‘ll set up the routes in App.js
:
// src/App.js
import React from ‘react‘;
import { BrowserRouter as Router, Route } from ‘react-router-dom‘;
import Container from ‘react-bootstrap/Container‘;
import ItemList from ‘./components/ItemList‘;
import ItemCreate from ‘./components/ItemCreate‘;
import ItemUpdate from ‘./components/ItemUpdate‘;
const App = () => {
return (
<Router>
<Container>
<Route path="/" exact component={ItemList} />
<Route path="/create" component={ItemCreate} />
<Route path="/update/:id" component={ItemUpdate} />
</Container>
</Router>
);
};
export default App;
Each Route
maps a URL path to a component. The exact
prop on the root path is necessary to ensure it doesn‘t match all paths.
To actually navigate between routes, we can use the Link
component from React Router:
// src/components/ItemList.js
import { Link } from ‘react-router-dom‘;
// ...
return (
<>
<Link to="/create">
<Button variant="primary">Create</Button>
</Link>
<ListGroup>
{items.map(item => (
<ListGroup.Item key={item.id}>
{item.title}
<Link to={`/update/${item.id}`}>
<Button variant="secondary" size="sm">
Update
</Button>
</Link>
<Button
variant="danger"
size="sm"
onClick={() => handleDelete(item.id)}
>
Delete
</Button>
</ListGroup.Item>
))}
</ListGroup>
</>
)
// ...
Here we‘ve added a "Create" button at the top of the list that navigates to the /create
route. Each list item also has an "Update" button that navigates to /update/:id
with the post ID.
With that, we have a fully functional CRUD application in React! You can run it with npm start
and test it out. Of course, since we‘re using a fake API, any changes won‘t persist if you refresh the page, but you should see the correct behavior and responses logged in the console.
Conclusion and Next Steps
We‘ve covered the basics of implementing CRUD functionality in a React application using hooks for state management and Axios for making API requests. You can use this as a starting point for your own projects and extend it with additional features like form validation, error handling, search/filtering, authentication and more.
Some other things to consider:
- Extracting the Axios logic into a separate service module for better organization and reusability
- Using a real backend API with a database to persist the data
- Adding loading states and spinners for better UX
- Implementing client-side routing with React Router
- Writing tests for your components
I hope this guide has been helpful in understanding how to put together a CRUD app in React. The full code is available on GitHub. Feel free to use it as a reference and reach out with any questions!
Happy coding!