I am building an app with reactjs tha needs to be real-time and I am using Rails Actioncable as a wrapper around websocket.
I can receive data via websocket after a record is created or updated and when I do console log to see what is contained in the posts array created with useHook but updated via webhook. It seems the post array is updated correctly using the code shown below. However react does not re-render the web page hence the use does not see that updated record.
import React, {useState, useEffect} from "react"
import consumer from "../../channels/consumer"
const PostList = (props) => {
const [posts, setPosts] = useState(props.posts || []);
useEffect(() => {
consumer.subscriptions.create({ channel: "PostChannel"}, {
received(data){
handleRecordUpdate(data);
}
})
}, [posts])
const handleRecordUpdate = (post) => {
//copying old array
let getPosts = posts;
let postIndex = getPosts.findIndex((obj => obj.id == post.id));
getPosts[postIndex] = post;
//ensure the new array does not reference the original array using spread operator ...
setPosts(posts => [...getPosts])
}
return (
<React.Fragment>
<h3>All Posts</h3>
<table>
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Description</th>
<th>Is Published</th>
<th> Actions </th>
</tr>
</thead>
<tbody>
{
posts && posts.map((post) => {
return (
<tr key={post.id} id={post.id}>
</tr>
)
})
}
</tbody>
</table>
</React.Fragment>
);
}
This is the structure of the array
{
"posts": [
{
"id": 2,
"title": "second",
"description": "push",
"is_published": true,
"created_at": "2021-04-03T13:59:03.708Z",
"updated_at": "2021-06-10T10:07:07.783Z"
},
{
"id": 3,
"title": "third",
"description": "testing 1",
"is_published": false,
"created_at": "2021-04-25T20:02:18.204Z",
"updated_at": "2021-06-10T10:15:18.724Z"
},
{
"id": 1,
"title": "good",
"description": "checkout",
"is_published": true,
"created_at": "2021-04-02T16:39:28.568Z",
"updated_at": "2021-06-10T15:25:42.186Z"
}
]
}
I fixed the issue with react not re-rendering when the state is updated via webhook. The primary problem was this line:
const [posts, setPosts] = useState(props.posts || []);
I changed that line to this:
const [posts, setPosts] = useState([]);
With that change this two approached updated the state with a re-render & broadcast of the state change via websockets to other open tabs.
Approach 1:
const handleRecordUpdate = (post) => {
let updatedPosts = [...posts]
let old_record = posts.find((item) => item.id === post.id)
let indexi = posts.indexOf(old_record)
updatedPosts[indexi] = post;
setPosts(posts => updatedPosts)
}
You can also use deep copy and it will work as suggested by @Mangesh.
const handleRecordUpdate = (post) => {
let updatedPosts = JSON.parse(JSON.stringify(posts))
let old_record = posts.find((item) => item.id === post.id)
let indexi = posts.indexOf(old_record)
updatedPosts[indexi] = post;
setPosts(posts => updatedPosts)
}
and finally this works too
const handleRecordUpdate = (post) => {
setPosts(posts.map(item => (item.id === post.id ? post : item) ))
}