Component doesnt rerender on state change - reactjs

My problem is that my component doesnt rerender, when my state changes. I am managing my state in a custom Hook and after an put request to my backend my state gets updated. This works completely fine, but the content of my page doesnt get refreshed when changing my sate after the put request.
Component:
import React, { useEffect, useState } from 'react';
import { CONTROLLERS, useBackend } from '../../hooks/useBackend';
import Loading from '../Alerts/loading';
import {Table} from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import DropdownForm from '../Forms/dropdown';
function AdminPanel() {
const headers = ['ID', 'Title', 'Release Date', 'Producer', 'Director', 'Status', 'UTC Time', '#', '#'];
const [error, setError] = useState(false);
const [loaded, setLoaded] = useState(false);
const [requests, backend] = useBackend(error, setError);
useEffect(() => {
backend(CONTROLLERS.REQUESTS.getRequestsAdmin());
}, [])
useEffect(() => {
setLoaded(requests !== undefined);
console.log(requests);
}, [requests])
const handleUpdate = (e, result) => {
backend(CONTROLLERS.REQUESTS.put({requestStatus: result, accessToken: localStorage.accessToken}, e));
}
if(!loaded) return <Loading/>
if(error) return <p>No Access</p>
return(
<>
<DropdownForm items={['A-Z', 'Z-A', 'None']} title={'Filter'} first={2} setHandler={setFilter}/>
<DropdownForm items={['+20', '+50', 'All']} title={'Count'} first={0} setHandler={setCount}/>
{/* <DropdownForm/> */}
<Table bordered hover responsive="md">
<thead>
<tr>
{headers.map((item, index) => {
return( <th className="text-center" key={index}>{item}</th> );
})}
</tr>
</thead>
<tbody>
{requests.map((item, index) =>{
return(
<tr>
<td>{index + 1}</td>
<td>{item.movie.movieTitle}</td>
<td>{item.movie.movieReleaseDate}</td>
<td>{item.movie.movieProducer}</td>
<td>{item.movie.movieDirector}</td>
<td>{(item.requestStatus === 1 ? 'Success' : item.requestStatus ===2 ? 'Pending' : 'Denied')}</td>
<td className="col-md-3">{item.requestDate}</td>
{/* <td><span onClick={() => handleDelete(item.requestID)}><i className="fas fa-times"></i></span></td> */}
<td><span onClick={() => handleUpdate(item.requestID, 3)}><i className="fas fa-times"></i></span></td>
<td><span onClick={() => handleUpdate(item.requestID, 1)}><i className="fas fa-check"></i></span></td>
</tr>);
})}
</tbody>
</Table>
</>
);
}
// }
export default AdminPanel;
customHook:
import axios from "axios";
import { useEffect, useRef, useState } from "react";
import notify from "../Components/Alerts/toasts";
const BASE_URL = 'https://localhost:44372/api/';
const R = 'Requests/'; const M = 'Movies/'; const U = 'Users/';
const buildParams = (url, type, header, param) => {
return {url: url, type: type, header: header, param: param};
}
export const CONTROLLERS = {
REQUESTS: {
getRequestsAdmin: () => buildParams(`${R}GetRequestsAdmin`, 'post', true, {accessToken:
}
export const useBackend = (error, setError) => {
const [values, setValues] = useState([]);
async function selectFunction(objc) {
switch(objc.type) {
case 'put': return buildPutAndFetch(objc.url, objc.param, objc.header);break;
default: console.log("Error in Switch");
}
}
async function buildPutAndFetch(url, param, header) {
const finalurl = `${BASE_URL}${url}`;
return axios.put(finalurl, param, {headers: {
'Authorization': `Bearer ${(localStorage.accessToken)}`
}})
.then(res => {
if(res.data && 'accessToken' in res.data) localStorage.accessToken = res.data.accessToken;
else {
//When an object gets updated, the backend returns the updated object and replaces the old one with the //new one.
const arr = values;
const found = values.findIndex(e => e[(Object.keys(res.data))[0]] == res.data.requestID);
arr[found] = res.data;
setValues(arr);
}
setError(false);
return true;
})
.catch(err => {
setError(true);
return false;
})
}
}
function response(res) {
setValues(res.data)
setError(false);
}
return [values,
async (objc) => selectFunction(objc)];
}

It's likely due to the fact that your buildPutAndFetch function is mutating the values array in state, rather than creating a new reference. React will bail out on state updates if the reference doesn't change.
When you declare your arr variable, it's setting arr equal to the same reference as values, rather than creating a new instance. You can use the spread operator to create a copy: const arr = [...values].
It's also worth noting that because this is happening asynchronously, you may want to use the function updater form of setValues to ensure you have the most current set of values when performing the update.
setValues(prev => {
const arr = [...prev];
const found = prev.findIndex((e) => e[Object.keys(res.data)[0]] == res.data.requestID);
arr[found] = res.data;
return arr;
});

Related

React | Collect State Values of Children Array and update Per Object and Save to PouchDB

Stackoverflow
problem
I have separate components that house Tiptap Editor tables. At first I had a save button for each Child Component which worked fine, but was not user friendly. I want to have a unified save button that will iterate through each child Table component and funnel all their editor.getJSON() data into an array of sections for the single doc object . Then finish it off by saving the whole object to PouchDB
What did I try?
link to the repo → wchorski/Next-Planner: a CRM for planning events built on NextJS (github.com)
Try #1
I tried to use the useRef hook and the useImperativeHandle to call and return the editor.getJSON(). But working with an Array Ref went over my head. I'll post some code of what I was going for
// Parent.jsx
const childrenRef = useRef([]);
childrenRef.current = []
const handleRef = (el) => {
if(el && !childrenRef.current.includes(el)){
childrenRef.current.push(el)
}
}
useEffect(() =>{
childrenRef.current[0].childFunction1() // I know this doesn't work, because this is where I gave up
})
// Child.jsx
useImperativeHandle(ref, () => ({
childFunction1() {
console.log('child function 1 called');
},
childFunction2() {
console.log('child function 2 called');
},
}))
Try #2
I set a state counter and passed it down as a prop to the Child Component . Then I update the counter to trigger a child function
// Parent.jsx
export const Planner = ({id, doc, rev, getById, handleSave, db, alive, error}) => {
const [saveCount, setSaveCount] = useState(0)
const handleUpdate = () =>{
setSaveCount(prev => prev + 1)
}
const isSections = () => {
if(sectionsState[0]) handleSave(sectionsState)
if(sectionsState[0] === undefined) console.log('sec 0 is undefined', sectionsState)
}
function updateSections(newSec) {
setsectionsState(prev => {
const newState = sectionsState.map(obj => {
if(!obj) return
if (obj.header === newSec.header) {
return {...obj, ...newSec}
}
// 👇️ otherwise return object as is
return obj;
});
console.log('newState', newState);
return newState;
});
}
useEffect(() => {
setsectionsState(doc.sections)
}, [doc])
return (<>
<button
title='save'
className='save'
onPointerUp={handleUpdate}>
Save to State <FiSave />
</button>
<button
style={{right: "0", width: 'auto'}}
title='save'
className='save'
onClick={isSections}>
Save to DB <FiSave />
</button>
{doc.sections.map((sec, i) => {
if(!sec) return
return (
<TiptapTable
key={i}
id={id}
rev={doc.rev}
getById={getById}
updateSections={updateSections}
saveCount={saveCount}
section={sec}
db={db}
alive={alive}
error={error}
/>
)
})}
</>)
// Child.jsx
export const TiptapTable = ((props, ref) => {
const {id, section, updateSections, saveCount} = props
const [currTimeStart, setTimeStart] = useState()
const [defTemplate, setdefTemplate] = useState('<p>loading<p>')
const [isLoaded, setIsLoaded] = useState(false)
const [notesState, setnotesState] = useState('')
const editor = useEditor({
extensions: [
History,
Document,
Paragraph,
Text,
Gapcursor,
Table.configure({
resizable: true,
}),
TableRow.extend({
content: '(tableCell | tableHeader)*',
}),
TableHeader,
TableCell,
],
// i wish it was this easy
content: (section.data) ? section.data : defTemplate,
}, [])
const pickTemplate = async (name) => {
try{
const res = await fetch(`/templates/${name}.json`,{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const data = await res.json()
setIsLoaded(true)
setdefTemplate(data)
console.log('defTemplate, ', defTemplate);
// return data
} catch (err){
console.warn('template error: ', err);
}
}
function saveData(){
console.log(' **** SAVE MEEEE ', section.header);
try{
const newSection = {
header: section.header,
timeStart: currTimeStart,
notes: notesState,
data: editor.getJSON(),
}
updateSections(newSection)
} catch (err){
console.warn('table update error: ', id, err);
}
}
useEffect(() => {
// 👇️ don't run on initial render
if (saveCount !== 0) saveData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [saveCount])
useEffect(() => {
setTimeStart(section.timeStart)
setnotesState(section.notes)
if(!section.data) pickTemplate(section.header).catch(console.warn)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, section, isLoaded])
useEffect(() => {
if (editor && !editor.isDestroyed) {
if(section.data) editor.chain().focus().setContent(section.data).run()
if(!section.data) editor.chain().focus().setContent(defTemplate).run()
setIsLoaded(true)
}
}, [section, defTemplate, editor]);
if (!editor) {
return null
}
return isLoaded ? (<>
<StyledTableEditor>
<div className="title">
<input type="time" label='Start Time' className='time'
onChange={(e) => setTimeStart(e.target.value)}
defaultValue={currTimeStart}
/>
<h2>{section.header}</h2>
</div>
<EditorContent editor={editor} className="tiptap-table" ></EditorContent>
// ... non relavent editor controls
<button
title='save'
className='save2'
onPointerUp={() => saveData()}>
Save <FiSave />
</button>
</div>
</nav>
</StyledTableEditor>
</>)
: null
})
TiptapTable.displayName = 'MyTiptapTable';
What I Expected
What I expected was the parent state to update in place, but instead it overwrites the previous tables. Also, once it writes to PouchDB it doesn't write a single piece of new data, just resolved back to the previous, yet with an updated _rev revision number.
In theory I think i'd prefer the useRef hook with useImperativeHandle to pass up the data from child to parent.
It looks like this question is similar but doesn't programmatically comb through the children
I realize I could have asked a more refined question, but instead of starting a new question I'll just answer my own question from what I've learned.
The problem being
I wasn't utilizing React's setState hook as I iterated and updated the main Doc Object
Thanks to this article for helping me through this problem.
// Parent.jsx
import React, {useState} from 'react'
import { Child } from '../components/Child'
export const Parent = () => {
const masterDoc = {
_id: "123",
date: "2023-12-1",
sections: [
{header: 'green', status: 'old'},
{header: 'cyan', status: 'old'},
{header: 'purple', status: 'old'},
]
}
const [saveCount, setSaveCount] = useState(0)
const [sectionsState, setsectionsState] = useState(masterDoc.sections)
function updateSections(inputObj) {
setsectionsState(prev => {
const newState = prev.map(obj => {
// 👇️ if id equals 2, update country property
if (obj.header === inputObj.header)
return {...obj, ...inputObj}
return obj;
});
return newState;
});
}
return (<>
<h1>Parent</h1>
{sectionsState.map((sec, i) => {
if(!sec) return
return (
<Child
key={i}
section={sec}
updateSections={updateSections}
saveCount={saveCount}
/>
)
})}
<button
onClick={() => setSaveCount(prev => prev + 1)}
>State dependant update {saveCount}</button>
</>)
}
// Child.jsx
import React, {useEffect, useState, forwardRef, useImperativeHandle} from 'react'
export const Child = forwardRef((props, ref) => {
const {section, updateSections, saveCount} = props
const [statusState, setStatusState] = useState(section.status)
function modData() {
const obj = {
header: section.header,
status: statusState
}
updateSections(obj)
}
useEffect(() => {
// 👇️ don't run on initial render
if (saveCount !== 0) modData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [saveCount])
return (<>
<span style={{color: section.header}}>
header: {section.header}
</span>
<span>status: {section.status}</span>
<input
defaultValue={section.status}
onChange={(e) => setStatusState(e.target.value)}
/>
________________________________________
</>)
})
Child.displayName = 'MyChild';

Why is my Saved State Overwritten on Refresh in this Simple React Example?

I am following this simple tutorial and can't get my saved state to work.
I can see in the comments that other users are having to work around this issue.
import React, { useState, useRef, useEffect } from "react"
import TodoList from "./TodoList"
import { v4 as uuidv4 } from 'uuid';
const LOCAL_STORAGE_KEY = 'todosApp.todos'
function App() {
const [todos, setTodos] = useState([])
const todoNameRef = useRef()
useEffect(() => {
console.log(`useEffect[]`)
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY));
if (storedTodos) {
console.log(`set todos to: ${JSON.stringify(storedTodos)}`)
setTodos(storedTodos)
// can't print here - value is set asynchronously
// console.log(`loaded todos: ${JSON.stringify(todos)}`)
}
}, [])
useEffect(() => {
console.log(`useEffect[todos]: ${JSON.stringify(todos)}`)
if (todos.length != 0) {
console.log('save')
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}
}, [todos])
function toggleTodo(id) {
const newTodos = [...todos]
const todo = newTodos.find(todo => todo.id === id)
console.log(`toggleTodo: ${todo.name}`)
todo.complete = !todo.complete
setTodos(newTodos)
}
function handleAddTodo(e) {
const name = todoNameRef.current.value
console.log(`handleAddTodo: ${name}`)
// setTodos(todos.concat({completed: false, name: todoNameRef.current.value}))
if (name === '') {
return
}
setTodos([...todos, { id:uuidv4(), name:name, complete:false }])
}
return (
<div>
<TodoList todos={todos} handleCheckboxChanged={toggleTodo}/>
<input ref={todoNameRef} type="text" />
<button onClick={handleAddTodo}>Add Todo</button>
<button>Clear Completed</button>
<div>0 left to do</div>
</div>
)
}
export default App;
Here's the output:
useEffect[]
App.js:15 set todos to: [{"id":"77fe1e9e-91aa-4a34-9bfb-b1842ea5518d","name":"asfd","complete":false},{"id":"8dabea66-4ed9-4f10-9003-af1b34b4558a","name":"asfd","complete":false},{"id":"6d4e9350-11cd-4ace-8766-485e1f8817ad","name":"asfd","complete":false}]
App.js:23 useEffect[todos]: []
App.js:12 useEffect[]
App.js:15 set todos to: []
App.js:23 useEffect[todos]: []
App.js:23 useEffect[todos]: []
So it seems like the state is asynchronously initialised after loading the state for some reason.
Here is my workaround:
useEffect(() => {
console.log(`useEffect[todos]: ${JSON.stringify(todos)}`)
if (todos.length != 0) {
console.log('save')
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}
}, [todos])
So I have a workaround, but why is this necessary? I can't wrap my head around how this can be intended React functionality.
useEffect(() => {
console.log(`useEffect[todos]: ${JSON.stringify(todos)}`)
if (todos.length != 0) {
console.log('save')
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}
}, [todos])
useEffect is always triggered initially, even though it has dependencies. In your case with the above snippet, it will be triggered twice:
Initial loading (like [] - no dependencies)
Updated todos state
So that's why it set empty data to localStorage because of initial loading without empty todos (if you don't have the condition todos.length != 0)
Your above snippet with the condition todos.length != 0 is reasonable, but it won't work for delete-all cases.
If you don't use any server-side rendering frameworks, you can set a default value for todos state
const [todos, setTodos] = useState(localStorage.getItem(LOCAL_STORAGE_KEY))
With this change, you can update useEffect like below
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todos))
}, [todos])
If you use a server-side rendering framework like NextJS, you can try to update localStorage directly on events (toggleTodo and handleAddTodo) instead of useEffect.
function App() {
const [todos, setTodos] = useState([])
const todoNameRef = useRef()
useEffect(() => {
console.log(`useEffect[]`)
const storedTodos = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY));
if (storedTodos) {
console.log(`set todos to: ${JSON.stringify(storedTodos)}`)
setTodos(storedTodos)
// can't print here - value is set asynchronously
// console.log(`loaded todos: ${JSON.stringify(todos)}`)
}
}, [])
function toggleTodo(id) {
const newTodos = [...todos]
const todo = newTodos.find(todo => todo.id === id)
console.log(`toggleTodo: ${todo.name}`)
todo.complete = !todo.complete
setTodos(newTodos)
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newTodos))
}
function handleAddTodo(e) {
const name = todoNameRef.current.value
console.log(`handleAddTodo: ${name}`)
// setTodos(todos.concat({completed: false, name: todoNameRef.current.value}))
if (name === '') {
return
}
const updatedTodos = [...todos, { id:uuidv4(), name:name, complete:false }]
setTodos(updatedTodos)
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(updatedTodos))
}
return (
<div>
<TodoList todos={todos} handleCheckboxChanged={toggleTodo}/>
<input ref={todoNameRef} type="text" />
<button onClick={handleAddTodo}>Add Todo</button>
<button>Clear Completed</button>
<div>0 left to do</div>
</div>
)
}

react-table, useState hook within table results to default value although set before

I am using react-table to display fetched data within a table. You also have different buttons within that table to interact with the data such as deleting an entry, or updating its data (toggle button to approve a submitted row).
The data is being fetched in an initial useEffect(() => fetchBars(), []) and then being passed to useTable by passing it through useMemo as suggested in the react-table documentation. Now I can click on the previously mentioned buttons within the table to delete an entry but when I try to access the data (bars) that has been set within fetchBars()it returns the default state used by useState() which is an empty array []. What detail am I missing? I want to use the bars state in order to filter deleted rows for example and thus make the table reactive, without having to re-fetch on every update.
When calling console.log(bars) within updateMyData() it displays the fetched data correctly, however calling console.log(bars) within handleApprovedUpdate() yields to the empty array, why so? Do I need to pass the handleApprovedUpdate() into the cell as well as the useTable hook as well?
const EditableCell = ({
value: initialValue,
row: { index },
column: { id },
row: row,
updateMyData, // This is a custom function that we supplied to our table instance
}: CellValues) => {
const [value, setValue] = useState(initialValue)
const onChange = (e: any) => {
setValue(e.target.value)
}
const onBlur = () => {
updateMyData(index, id, value)
}
useEffect(() => {
setValue(initialValue)
}, [initialValue])
return <EditableInput value={value} onChange={onChange} onBlur={onBlur} />
}
const Dashboard: FC<IProps> = (props) => {
const [bars, setBars] = useState<Bar[]>([])
const [loading, setLoading] = useState(false)
const COLUMNS: any = [
{
Header: () => null,
id: 'approver',
disableSortBy: true,
Cell: (props :any) => {
return (
<input
id="approved"
name="approved"
type="checkbox"
checked={props.cell.row.original.is_approved}
onChange={() => handleApprovedUpdate(props.cell.row.original.id)}
/>
)
}
}
];
const defaultColumn = React.useMemo(
() => ({
Filter: DefaultColumnFilter,
Cell: EditableCell,
}), [])
const updateMyData = (rowIndex: any, columnId: any, value: any) => {
let barUpdate;
setBars(old =>
old.map((row, index) => {
if (index === rowIndex) {
barUpdate = {
...old[rowIndex],
[columnId]: value,
}
return barUpdate;
}
return row
})
)
if(barUpdate) updateBar(barUpdate)
}
const columns = useMemo(() => COLUMNS, []);
const data = useMemo(() => bars, [bars]);
const tableInstance = useTable({
columns: columns,
data: data,
initialState: {
},
defaultColumn,
updateMyData
}, useFilters, useSortBy, useExpanded );
const fetchBars = () => {
axios
.get("/api/allbars",
{
headers: {
Authorization: "Bearer " + localStorage.getItem("token")
}
}, )
.then(response => {
setBars(response.data)
})
.catch(() => {
});
};
useEffect(() => {
fetchBars()
}, []);
const handleApprovedUpdate = (barId: number): void => {
const approvedUrl = `/api/bar/approved?id=${barId}`
setLoading(true)
axios
.put(
approvedUrl, {},
{
headers: {Authorization: "Bearer " + localStorage.getItem("token")}
}
)
.then(() => {
const updatedBar: Bar | undefined = bars.find(bar => bar.id === barId);
if(updatedBar == null) {
setLoading(false)
return;
}
updatedBar.is_approved = !updatedBar?.is_approved
setBars(bars.map(bar => (bar.id === barId ? updatedBar : bar)))
setLoading(false)
})
.catch((error) => {
setLoading(false)
renderToast(error.response.request.responseText);
});
};
const renderTable = () => {
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow
} = tableInstance;
return(
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>
<span {...column.getSortByToggleProps()}>
{column.render('Header')}
</span>{' '}
<span>
{column.isSorted ? column.isSortedDesc ? ' ▼' : ' ▲' : ''}
</span>
<div>{column.canFilter ? column.render('Filter') : <Spacer/>}</div>
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row)
const rowProps = {...row.getRowProps()}
delete rowProps.role;
return (
<React.Fragment {...rowProps}>
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
)
})}
</tr>
{row.isExpanded ? renderRowSubComponent({row}): null}
</React.Fragment>
)})
}
</tbody>
</table>
)
}
}
export default Dashboard;
You're seeing stale values within handleApprovedUpdate because it's capturing bars the first time the component is rendered, then never being updated since you're using it inside COLUMNS, which is wrapped with a useMemo with an empty dependencies array.
This is difficult to visualize in your example because it's filtered through a few layers of indirection, so here's a contrived example:
function MyComponent() {
const [bars, setBars] = useState([]);
const logBars = () => {
console.log(bars);
};
const memoizedLogBars = useMemo(() => logBars, []);
useEffect(() => {
setBars([1, 2, 3]);
}, []);
return (
<button onClick={memoizedLogBars}>
Click me!
</button>
);
}
Clicking the button will always log [], even though bars is immediately updated inside the useEffect to [1, 2, 3]. When you memoize logBars with useMemo and an empty dependencies array, you're telling React "use the value of bars you can currently see, it will never change (I promise)".
You can resolve this by adding bars to the dependency array for useMemo.
const memoizedLogBars = useMemo(() => logBars, [bars]);
Now, clicking the button should correctly log the most recent value of bars.
In your component, you should be able to resolve your issue by changing columns to
const columns = useMemo(() => COLUMNS, [bars]);
You can read more about stale values in hooks here. You may also want to consider adding eslint-plugin-react-hooks to your project setup so you can identify issues like this automatically.

JSON Array mapping in ReactJS from request

Currently i'm rewriting a class component to a function component. I need to do this since i need to use the useSelector hook from redux. Now i'm getting pretty close but i'm having some trouble with the json array getting mapped. It's letting me know it's not a function. In the fetch i'm logging the leaderboard which has returned. This gives me the json i was expecting.
[
{
"ID": 1,
"teamName": "Developers",
"time": "19:54"
},
{
"ID": 1591621934400,
"teamName": "h435hfg",
"time": "19:54"
}
]
Then here is my code that im having trouble with:
import React, {useEffect, useState} from 'react';
import '../style/App.scss';
import {useSelector} from "react-redux";
function Leaderboard() {
const io = require('socket.io-client');
const socket = io.connect("http://localhost:3001/", {
reconnection: false
});
const [leaderboard, setLeaderboard] = useState([]);
const timerState = useSelector(state => state.timerState);
useEffect(() => {
socket.emit("addTeamToLeaderboard", getTeam());
fetch('http://localhost:3000/leaderboard')
.then(response => response.json())
.then(leaderboard => {
leaderboard.push(getTeam()); // this is just so your team score renders the first time
setLeaderboard({leaderboard})
console.log(leaderboard)
});
}, [socket]);
const getTeam = () => {
let team = JSON.parse(sessionStorage.getItem('currentTeam')) ;
team.time = timerState;
return team;
}
const leaderboardElements = leaderboard.map((data, key) => {
return (
<tr key={key} className={ data.ID === getTeam().ID ? "currentTeam" : "" }>
<td>{data.teamName}</td>
<td>{data.time}</td>
</tr>
)
})
return (
<div>
<h1>Leaderboard</h1>
<table className="leaderboard">
<tr>
<th>Team</th>
<th>Time</th>
</tr>
{leaderboardElements}
</table>
</div>
);
}
export default Leaderboard;
The old code which im rewriting:
import React from 'react';
import '../style/App.scss';
class Leaderboard extends React.Component {
state = {
leaderboard: []
}
compare(a, b) {
if (a.time < b.time) {
return -1;
}
if (a.time > b.time) {
return 1;
}
return 0;
}
getTeam(){
let team = JSON.parse(sessionStorage.getItem('currentTeam')) ;
team.time = 12.13; //Todo add actual playing time
return team;
}
componentDidMount() {
const io = require('socket.io-client');
const socket = io.connect("http://localhost:3001/", {
reconnection: false
});
socket.emit("addTeamToLeaderboard", this.getTeam());
fetch('http://localhost:3000/leaderboard')
.then(response => response.json())
.then(leaderboard => {
leaderboard.push(this.getTeam()); // this is just so your team score renders the first time
this.setState({ leaderboard })
});
}
render() {
return (
<div>
<h1>Leaderboard</h1>
<table className="leaderboard">
<tr>
<th>Team</th>
<th>Time</th>
</tr>
{
this.state.leaderboard.sort(this.compare).map((data, key) => {
return (
<tr key={key} className={ data.ID == this.getTeam().ID ? "currentTeam" : "" }>
<td>{data.teamName}</td>
<td>{data.time}</td>
</tr>
)
})
}
</table>
</div>
);
}
}
export default Leaderboard;
I'm not following why you are changing leaderboard data type. If it is an array you shouldn't do setLeaderboard({leaderboard}) because you are assigning an object to the state.
You should pass a new array to the setLeaderboard like:
setLeaderboard([...leaderboard]);
Also if you do
setLeaderboard([...leaderboard]);
console.log(leaderboard);
You will not get the updated state right in the log, because set state is an asynchronous call.
Another tip, I would highly recommend you to put the socket connection not in the useEffect function, put outside the functional component.
const io = require('socket.io-client');
const socket = io.connect("http://localhost:3001/", {
reconnection: false
});
function Leaderboard() {
...
}
It's letting me know it's not a function
/* fetch data */
leaderboard.push(getTeam());
setLeaderboard({leaderboard}) // => change to setLeaderboard(leaderboard.concat(getTeam()))
console.log(leaderboard)
/* other functions below */
the difference between setState and the setLeaderboard that is returned from useState is that (when giving none callback argument)
setState expects an object with {[key: stateYouAreChanging]: [value: newState],
setLeaderboard expects the newStatValue as the argument.
So your code above is setting leaderboard state to be an object with that looks like this
leaderboard = {
leaderboard: NEW_LEADERBOARD_FETCHED_FROM_REQUEST
}

React hook useEffect failed to read new useState value that is updated with firebase's firestore realtime data

I have an array of data object to be rendered. and this array of data is populated by Firestore onSnapshot function which i have declared in the React hook: useEffect. The idea is that the dom should get updated when new data is added to firestore, and should be modified when data is modified from the firestore db.
adding new data works fine, but the problem occurs when the data is modified.
here is my code below:
import React, {useState, useEffect} from 'react'
...
const DocList = ({firebase}) => {
const [docList, setDocList] = useState([]);
useEffect(() => {
const unSubListener = firebase.wxDocs()
.orderBy("TimeStamp", "asc")
.onSnapshot({
includeMetadataChanges: true
}, docsSnap => {
docsSnap.docChanges()
.forEach(docSnap => {
let source = docSnap.doc.metadata.fromCache ? 'local cache' : 'server';
if (docSnap.type === 'added') {
setDocList(docList => [{
source: source,
id: docSnap.doc.id,
...docSnap.doc.data()
}, ...docList]);
console.log('document added: ', docSnap.doc.data());
} // this works fine
if (docSnap.type === 'modified') {
console.log('try docList from Lists: ', docList); //this is where the problem is, this returns empty array, i don't know why
console.log('document modified: ', docSnap.doc.data()); //modified data returned
}
})
})
return () => {
unSubListener();
}
}, []);
apparently, i know the way i declared the useEffect with empty deps array is to make it run once, if i should include docList in the deps array the whole effect starts to run infinitely.
please, any way around it?
As commented, you could have used setDocList(current=>current.map(item=>..., here is working example with fake firebase:
const firebase = (() => {
const createId = ((id) => () => ++id)(0);
let data = [];
let listeners = [];
const dispatch = (event) =>
listeners.forEach((listener) => listener(event));
return {
listen: (fn) => {
listeners.push(fn);
return () => {
listeners = listeners.filter((l) => l !== fn);
};
},
add: (item) => {
const newItem = { ...item, id: createId() };
data = [...data, newItem];
dispatch({ type: 'add', doc: newItem });
},
edit: (id) => {
data = data.map((d) =>
d.id === id ? { ...d, count: d.count + 1 } : d
);
dispatch({
type: 'edit',
doc: data.find((d) => d.id === id),
});
},
};
})();
const Counter = React.memo(function Counter({ up, item }) {
return (
<button onClick={() => up(item.id)}>
{item.count}
</button>
);
});
function App() {
const [docList, setDocList] = React.useState([]);
React.useEffect(
() =>
firebase.listen(({ type, doc }) => {
if (type === 'add') {
setDocList((current) => [...current, doc]);
}
if (type === 'edit') {
setDocList((current) =>
current.map((item) =>
item.id === doc.id ? doc : item
)
);
}
}),
[]
);
const up = React.useCallback(
(id) => firebase.edit(id),
[]
);
return (
<div>
<button onClick={() => firebase.add({ count: 0 })}>
add
</button>
<div>
{docList.map((doc) => (
<Counter key={doc.id} up={up} item={doc} />
))}
</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You can do setDocList(docList.map... but that makes docList a dependency of the effect: useEffect(function,[docList]) and the effect will run every time docList changes so you need to remove the listener and idd it every time.
In your code you did not add the dependency so docList was a stale closure. But the easiest way would be to do what I suggested and use callback for setDocList: setDocList(current=>current.map... so docList is not a dependency of the effect.
The comment:
I don't think setDocList, even with the the prevState function, is guaranteed to be up to date by the time you get into that if statement
Is simply not true, when you pass a callback to state setter the current state is passed to that callback.
Based on #BrettEast suggestion;
I know this isn't what you want to hear, but I would probably suggest using useReducer reactjs.org/docs/hooks-reference.html#usereducer, rather than useState for tracking an array of objects. It can make updating easier to track. As for your bug, I don't think setDocList, even with the the prevState function, is guaranteed to be up to date by the time you get into that if statement.
I use useReducer instead of useState and here is the working code:
import React, {useReducer, useEffect} from 'react'
import { withAuthorization } from '../../Session'
import DocDetailsCard from './Doc';
const initialState = [];
/**
* reducer declaration for useReducer
* #param {[*]} state the current use reducer state
* #param {{payload:*,type:'add'|'modify'|'remove'}} action defines the function to be performed and the data needed to execute such function in order to modify the state variable
*/
const reducer = (state, action) => {
switch (action.type) {
case 'add':
return [action.payload, ...state]
case 'modify':
const modIdx = state.findIndex((doc, idx) => {
if (doc.id === action.payload.id) {
console.log(`modified data found in idx: ${idx}, id: ${doc.id}`);
return true;
}
return false;
})
let newModState = state;
newModState.splice(modIdx,1,action.payload);
return [...newModState]
case 'remove':
const rmIdx = state.findIndex((doc, idx) => {
if (doc.id === action.payload.id) {
console.log(`data removed from idx: ${idx}, id: ${doc.id}, fullData: `,doc);
return true;
}
return false;
})
let newRmState = state;
newRmState.splice(rmIdx,1);
return [...newRmState]
default:
return [...state]
}
}
const DocList = ({firebase}) => {
const [state, dispatch] = useReducer(reducer, initialState)
useEffect(() => {
const unSubListener = firebase.wxDocs()
.orderBy("TimeStamp", "asc")
.onSnapshot({
includeMetadataChanges: true
}, docsSnap => {
docsSnap.docChanges()
.forEach(docSnap => {
let source = docSnap.doc.metadata.fromCache ? 'local cache' : 'server';
if (docSnap.type === 'added') {
dispatch({type:'add', payload:{
source: source,
id: docSnap.doc.id,
...docSnap.doc.data()
}})
}
if (docSnap.type === 'modified') {
dispatch({type:'modify',payload:{
source: source,
id: docSnap.doc.id,
...docSnap.doc.data()
}})
}
if (docSnap.type === 'removed'){
dispatch({type:'remove',payload:{
source: source,
id: docSnap.doc.id,
...docSnap.doc.data()
}})
}
})
})
return () => {
unSubListener();
}
}, [firebase]);
return (
<div >
{
state.map(eachDoc => (
<DocDetailsCard key={eachDoc.id} details={eachDoc} />
))
}
</div>
)
}
const condition = authUser => !!authUser ;
export default React.memo(withAuthorization(condition)(DocList));
also according to #HMR, using the setState callback function:
here is the updated code which also worked if you're to use useState().
import React, { useState, useEffect} from 'react'
import { withAuthorization } from '../../Session'
import DocDetailsCard from './Doc';
const DocList = ({firebase}) => {
const [docList, setDocList ] = useState([]);
const classes = useStyles();
useEffect(() => {
const unSubListener = firebase.wxDocs()
.orderBy("TimeStamp", "asc")
.onSnapshot({
includeMetadataChanges: true
}, docsSnap => {
docsSnap.docChanges()
.forEach(docSnap => {
let source = docSnap.doc.metadata.fromCache ? 'local cache' : 'server';
if (docSnap.type === 'added') {
setDocList(current => [{
source: source,
id: docSnap.doc.id,
...docSnap.doc.data()
}, ...current]);
console.log('document added: ', docSnap.doc.data());
}
if (docSnap.type === 'modified') {
setDocList(current => current.map(item => item.id === docSnap.doc.id ? {
source: source,
id: docSnap.doc.id,
...docSnap.doc.data()} : item )
)
}
if (docSnap.type === 'removed'){
setDocList(current => {
const rmIdx = current.findIndex((doc, idx) => {
if (doc.id === docSnap.doc.id) {
return true;
}
return false;
})
let newRmState = current;
newRmState.splice(rmIdx, 1);
return [...newRmState]
})
}
})
})
return () => {
unSubListener();
}
}, [firebase]);
return (
<div >
{
docList.map(eachDoc => (
<DocDetailsCard key={eachDoc.id} details={eachDoc} />
))
}
</div>
)
}
const condition = authUser => !!authUser ;
export default React.memo(withAuthorization(condition)(DocList));
Thanks hope this help whoever is experiencing similar problem.

Resources