Josh Lee
Posted on June 3, 2021
Welcome to part 3 of my React on Ruby on Rails (React on Rails?, RX3?, RoRoR?). Today, we’re going to add CRUD functions to our app. We already have it set up on the backend, now we just need to connect our frontend. This should be relatively easy.
Here is the code for our api in “app/javascript/src/api/api.js”
import axios from 'axios'
const ROOT_PATH = '/api/v1'
const POSTS_PATH = `${ROOT_PATH}/posts`
export const getPosts = () => {
return axios.get(POSTS_PATH)
}
export const getPost = (postId) => {
return axios.get(`${POSTS_PATH}/${postId}`)
}
export const createPost = (postParams) => {
return axios.post(POSTS_PATH, postParams)
}
export const destroyPost = (postId) => {
return axios.delete(`${POSTS_PATH}/${postId}`)
}
export const updatePost = (postId, postParams) => {
return axios.put(`${POSTS_PATH}/${postId}`, postParams)
}
These are our CRUD functions that will connect to the database. I think these are pretty explanatory. The only thing worth noting is that with createPost() and updatePost() you need to make sure you pass in the params as the second argument.
Now, let’s go to our types file and make sure we have the correct types for our action creators and reducers. This is in “app/javascript/src/types/index.js”.
export const GET_POSTS = "GET_POSTS"
export const GET_POST = "GET_POST"
export const CREATE_POST = "CREATE_POST"
export const DESTROY_POST = "DESTROY_POST"
export const UPDATE_POST = "UPDATE_POST"
Now we just need to go to our action creator and make sure we’re making requests to our rails backend while sending the action types to our reducers. This file is “app/javascript/src/actions/posts.js”.
import * as api from '../api/api'
import { GET_POST, GET_POSTS, CREATE_POST, UPDATE_POST, DESTROY_POST } from '../types/index'
export const getPosts = () => async (dispatch) => {
try {
const { data } = await api.getPosts()
dispatch({
type: GET_POSTS,
payload: data.data
})
} catch (error) {
console.log(error)
}
}
export const getPost = (postId) => async (dispatch) => {
try {
const { data } = await api.getPost(postId)
dispatch({
type: GET_POST,
payload: data.data
})
} catch (error) {
console.log(error)
}
}
export const createPost = (postParams) => async (dispatch) => {
try {
const { data } = await api.createPost(postParams)
dispatch({
type: CREATE_POST,
payload: data.data
})
} catch (error) {
console.log(error)
}
}
export const updatePost = (postId, postParams) => async (dispatch) => {
try {
const { data } = await api.updatePost(postId, postParams)
dispatch({
type: UPDATE_POST,
payload: data.data
})
} catch (error) {
console.log(error)
}
}
export const destroyPost = (postId) => async (dispatch) => {
try {
const { data } = await api.destroyPost(postId)
dispatch({
type: DESTROY_POST,
payload: postId
})
} catch (error) {
console.log(error)
}
}
Let’s look at one of these functions and see exactly what it’s doing. Let’s look at the createPost() function.
export const createPost = (postParams) => async (dispatch) => {
try {
const { data } = await api.createPost(postParams)
dispatch({
type: CREATE_POST,
payload: data.data
})
} catch (error) {
console.log(error)
}
}
Here, we’re creating a function called createPost that takes an argument of postParams. Then, we state that it’s an async function and we want to use redux-thunk.
Next, we start a try-and-catch block. We use our api to make a call to the backend and take the output and put it in the const data.
Then, we tell all of our reducers that we are creating a CREATE_POST action and passing in the data so the reducers can use the data from the backend to update our redux store.
Finally, we’re logging any errors.
Now, we need to take care of these actions with our reducers. Let’s start with the GET_POST action type. This sets the current post, so we need to create a reducer for it.
Create the file “app/javascript/src/reducers/post.js” and put this there.
import { GET_POST } from '../types/index'
export default (post = null, action ) => {
switch (action.type) {
case GET_POST:
return action.payload
default:
return post
}
}
We’re setting the initial post to null and then we’re telling this reducer whenever it sees the GET_POST action, take that payload and assign it to the post key on our redux store. Make sure to add this reducer to your “app/javascript/src/reducers/index.js” file.
import { combineReducers } from 'redux'
import posts from './posts'
import post from './post'
export default combineReducers({
posts,
post
})
We’re going to make a post#show page, but before we do that we need to set up our router. In “app/javascript/src/components/App.js” we need to import the Page component we’re going to make and then tell the router to render that component when we go to /post/:id.
After adding the code, your App.js should look like the following:
import React from 'react'
import { Route, Switch } from 'react-router-dom'
import Posts from '../components/Posts/Posts'
import Post from '../components/Posts/Post'
const App = () => {
return (
<Switch>
<Route exact path="/" component={Posts} />
<Route exact path="/posts/:id" component={Post} />
</Switch>
)
}
export default App
In our PostListItem component, we’re going to add links to the individual Post components. Since we’re working with React Router Dome, we can’t just use tags. Instead, we have to import Link from React Router. The component should look like the following:
import React from 'react'
import { Link } from 'react-router-dom'
const PostListItem = ({post}) => {
return(
<div>
<Link to={`/posts/${post.id}`}>
<h2>{post.attributes.title}</h2>
</Link>
<p>{post.attributes.body}</p>
</div>
)
}
export default PostListItem
We can’t run our app right now because we’re importing the Post component that doesn’t exist. Let’s make a quick component so we can see if everything is working right now.
Read
Make a component at “app/javascript/src/components/Posts/Post” with the following:
import React from 'react'
const Post = () => {
return(
<div>
<h1>This is the Post Component</h1>
</div>
)
}
export default Post
Go to “http://localhost:3000/posts/123” and you should see your new Post component.
You can also go to http://localhost:3000/ and check the links we put there to link to the specific Post component.
We have our Post component, now let’s connect it to our api. We’re going to set up our component to fetch the post when the component renders, and then once it gets the data it will render again, this time with the new data.
This is what’s going to set up our component with the correct data it needs:
import React, { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { getPost } from '../../actions/posts'
const Post = ({match}) => {
const dispatch = useDispatch()
const post = useSelector(state => state.post)
const postId = match.params.id
useEffect(() => {
dispatch(getPost(postId))
}, [])
return(
<div>
<h1>This is the Post Component</h1>
</div>
)
}
export default Post
Here, we’re getting the id from the url and then using that Id to fetch the post data from our backend. For more explanation on useDispatch and useSelector, see part 2.
Note, if you’re following along from my previous tutorials, I misplaced an “end” in my controller. I had to fix that before moving on.
Now, it’s just a matter of populating the page with the information from our post.
import React, { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { getPost } from '../../actions/posts'
const Post = ({match}) => {
const dispatch = useDispatch()
const post = useSelector(state => state.post)
const postId = match.params.id
useEffect(() => {
dispatch(getPost(postId))
}, [])
if (!post) { return <div>Loading....</div>}
return(
<div>
<h1>{post.attributes.title}</h1>
<p>{post.attributes.body}</p>
</div>
)
}
export default Post
And there you have it! That’s the R from CRUD. Now, let’s get on to creating records from the front end.
Create
First, we need to create the form in “app/javascript/src/components/Posts.New”. This is what the form looks like:
import React, { useState } from 'react'
import { useDispatch } from 'react-redux'
import { createPost } from '../../actions/posts'
import { useHistory } from "react-router-dom";
const New = () => {
const dispatch = useDispatch()
const history = useHistory()
const [formData, setFormData] = useState({
title: "",
body: ""
})
const handleSubmit = (e) => {
e.preventDefault()
dispatch(createPost({post: formData}))
history.push("/")
}
return (
<div>
<h1>New Post</h1>
<form onSubmit={handleSubmit}>
<label htmlFor="title">Title</label>
<input onChange={(e) => setFormData({...formData, title: e.target.value})} type="text" name="title" id="title" value={formData.title} />
< br />
<label htmlFor="body">Body</label>
<textarea onChange={(e) => setFormData({...formData, body: e.target.value})} name="body" id="body" cols={30} rows={10} value={formData.body}></textarea>
< br />
<input type="submit" value="Create Post" />
</form>
</div>
)
}
export default New
If this code looks confusing to you, I have an article about working with forms in React.
In this form, we’re creating a new Post object and then redirecting back to the home page. If you try to send this to your backend now, you’ll an error. You need to go to your posts_controller.rb and add the following:
protect_from_forgery with: :null_session
Let’s add the last part to our reducer. This is just updating the posts key in our Redux store.
import { GET_POSTS, CREATE_POST } from '../types/index'
export default (posts = [], action ) => {
switch (action.type) {
case GET_POSTS:
return action.payload
case CREATE_POST:
return [...posts, action.payload]
default:
return posts
}
}
If you’ve followed everything so far, it should be working and now we’ve finished the Create in our CRUD.
Destroy
Now it’s time to destroy our model. We already have the action creator set up. We need to configure our reducers. First, we need to remove the post from our posts key with the DESTROY_POST action type in our Redux store like so:
import { GET_POSTS, CREATE_POST, DESTROY_POST } from '../types/index'
export default (posts = [], action ) => {
switch (action.type) {
case GET_POSTS:
return action.payload
case CREATE_POST:
return [...posts, action.payload]
case DESTROY_POST:
return posts.filter(post => post.id != action.payload)
default:
return posts
}
}
We’re just going through our posts and filtering out the post we just deleted. Next, let’s set our post to null in our post reducer:
import { GET_POST, DESTROY_POST } from '../types/index'
export default (post = null, action ) => {
switch (action.type) {
case GET_POST:
return action.payload
case DESTROY_POST:
return null
default:
return post
}
}
The reason I’m doing this is that when we’re deleting the post, the post is also set as the post key in our Redux store.
Next, let’s create a new component at “app/javascript/src/components/Posts/Edit.js” with the following code
import React, { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { getPost, destroyPost } from '../../actions/posts'
import { useHistory } from "react-router-dom";
const Edit = ({ match }) => {
const dispatch = useDispatch()
const history = useHistory()
const post = useSelector(state => state.post)
const postId = match.params.id
const handleClick = () => {
dispatch(destroyPost(postId))
history.push("/")
}
useEffect(() => {
dispatch(getPost(postId))
}, [])
if (!post) { return <div>Loading...</div>}
return (
<div>
<h1>{post.attributes.title}</h1>
<button onClick={handleClick}>Delete me</button>
</div>
)
}
export default Edit
This should all look familiar to you now – we’re just deleting this time. And make sure to add the route to your App.js file.
import React from 'react'
import { Route, Switch } from 'react-router-dom'
import Posts from '../components/Posts/Posts'
import Post from '../components/Posts/Post'
import New from '../components/Posts/New'
import Edit from '../components/Posts/Edit'
const App = () => {
return (
<Switch>
<Route exact path="/" component={Posts} />
<Route exact path="/posts/new" component={New} />
<Route exact path="/posts/:id" component={Post} />
<Route exact path="/posts/:id/edit" component={Edit} />
</Switch>
)
}
export default App
And there we have it – destroy is done. One more and then we’re finished!
Update
We’re going to use our Posts/Edit.js component we just made for the delete action. In this component, we just need to set up a form just like creating a new Post.
Your Posts/Edit.js file should look like the following:
import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { getPost, destroyPost, updatePost } from '../../actions/posts'
import { useHistory } from "react-router-dom";
const Edit = ({ match }) => {
const dispatch = useDispatch()
const history = useHistory()
const post = useSelector(state => state.post)
const postId = match.params.id
const handleClick = () => {
dispatch(destroyPost(postId))
history.push("/")
}
const [formData, setFormData] = useState({
title: '',
body: ''
})
useEffect(() => {
dispatch(getPost(postId))
}, [])
useEffect(() => {
if (post) {
setFormData({
title: post.attributes.title || '',
body: post.attributes.body || ''
})
}
}, [post])
if (!post) { return <div>Loading...</div>}
const handleSubmit = (e) => {
e.preventDefault()
dispatch(updatePost(postId, {post: formData}))
history.push("/")
}
return (
<div>
<form onSubmit={handleSubmit}>
<h1>{post.attributes.title}</h1>
<label htmlFor="title">Title</label>
<input onChange={(e) => setFormData({...formData, title: e.target.value})} type="text" name="title" id="title" value={formData.title} />
<br />
<label htmlFor="body">Body</label>
<textarea onChange={(e) => setFormData({...formData, body: e.target.value})} name="body" id="body" cols={30} rows={10} value={formData.body}></textarea>
<br />
<button onClick={handleClick}>Delete me</button>
<input type="Submit" value="Save" />
</form>
</div>
)
}
export default Edit
This is similar to our create method – we have a form setup and we’re using our action creator updatePost(). The only thing that might look strange here is this part:
useEffect(() => {
if (post) {
setFormData({
title: post.attributes.title || '',
body: post.attributes.body || ''
})
}
}, [post])
See that [post] there? Whenever the value post changes, this useEffect() hook runs. That means after we contact the backend and update Redux store with post, this function runs and sets the default values for our form.
The last thing we have to do is up this in our posts in Redux store. In “app/javascript/src/reducers/posts.js” add UPDATE_POST:
import { GET_POSTS, CREATE_POST, DESTROY_POST, UPDATE_POST } from '../types/index'
export default (posts = [], action ) => {
switch (action.type) {
case GET_POSTS:
return action.payload
case CREATE_POST:
return [...posts, action.payload]
case DESTROY_POST:
return posts.filter(post => post.id != action.payload)
case UPDATE_POST:
let updatedPosts = posts.map(post => {
if (post.id === action.payload.id) {
return action.payload
} else {
return post
}
})
return updatedPosts
default:
return posts
}
}
Here, we’re just mapping through our posts and finding the post we just updated. Then we are replacing the old post with the new post.
And there we have it. We have now implemented CRUD functions into our React on Rails app. I plan on doing authentication next. Make sure follow me on Twitter so you know when I published it.
Posted on June 3, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
August 27, 2024