How to replicate this set state logic using hooks - reactjs

I am in the process of converting this React class to a function. One very used & important part of it is making sure that the this.state(args, callback) works like the method below.
fetchMore = () => {
this.setState(
{
skip: this.state.skip + 30,
},
async () => {
const { skip, take } = this.state
const params = {
skip,
take,
order: "start-desc",
filter: this.applyFilters(),
}
this.fetchTournaments(params, false)
},
)
}
I am not very familiar with using react hooks, How would this work hooks?

You can refer this working demo and below code snippet
export default function App() {
const [skip, setSkips] = useState(0);
const [take, setTake] = useState(10);
useEffect(() => {
const params = {
skip,
take,
order: "start-desc"
// filter: applyFilters() // make sure to create a applyFilters fun
};
// fetchTournaments(params, false); // make sure to create a fetchTournaments fun
}, [skip]);
const fetchMore = () => {
setSkips(prev => prev + 30);
};
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<button onClick={fetchMore}>Fetch More</button>
{"skip ---- " + skip}
{"take ---- " + take}
</div>
);
}

Related

React can't get component to update when state changes

I am trying to update a videoId using state, but when the state changes, it does not update my component for some reason.
I've tried to spread the new data, but that isn't working either.
Here is my code:
const exercises = [
{
id: 1,
videoSrc:video,
},
{
id: 2,
videoSrc: video
},
{
id: 3,
videoSrc: video
},
{
id: 4,
videoSrc: video
},
];
export function WarmUpContent() {
const [videoId, setVideoId] = useState(1);
const [videoSrc, setVideoSrc] = useState(exercises[0].videoSrc);
function handleNext() {
setVideoId(videoId + 1);
console.log(videoId);
const result = exercises.find(({ id }) => id === videoId);
const video = result.videoSrc;
setVideoSrc([...video]);
}
return (
<>
<div className="">
<div className="mb-4">
<VideoPlayer src={videoSrc} />
</div>
<div className="">
<Button onClick={() => handleNext()} size="slim">
Next
</Button>
</div>
</div>
</>
);
}
The state is updated asynchronously, so the new state would only be available on the next render. A simple solution is to calculate the new id, and then set the state, and use to get the video src:
function handleNext() {
const newId = videoId + 1;
setVideoId(newId);
const result = exercises.find(({ id }) => id === newId);
const video = result.videoSrc;
setVideoSrc(video); // don
}
However, a better solution would be use only a single state (videoId). Update the videoId and then calculate the derived data (videoSrc) during render:
export function WarmUpContent() {
const [videoId, setVideoId] = useState(1);
function handleNext() {
setVideoId(id => id + 1); // use the update function when the value is dependent on prev value
}
const videoSrc = useMemo(() => {
const result = exercises.find(({ id }) => id === videoId);
return result.videoSrc;
}, [videoId]);
return (...);
}

react-countdown is not reseting or re-rendering second time

What I am trying to do is to update the reset the countdown after changing the status.
There are three status that i am fetching from API .. future, live and expired
If API is returning future with a timestamp, this timestamp is the start_time of the auction, but if the status is live then the timestamp is the end_time of the auction.
So in the following code I am calling api in useEffect to fetch initial data pass to the Countdown and it works, but on 1st complete in handleRenderer i am checking its status and updating the auctionStatus while useEffect is checking the updates to recall API for new timestamp .. so far its working and 2nd timestamp showed up but it is stopped ... means not counting down time for 2nd time.
import React, { useEffect } from 'react';
import { atom, useAtom } from 'jotai';
import { startTimeAtom, auctionStatusAtom } from '../../atoms';
import { toLocalDateTime } from '../../utility';
import Countdown from 'react-countdown';
import { getCurrentAuctionStatus } from '../../services/api';
async function getAuctionStatus() {
let response = await getCurrentAuctionStatus(WpaReactUi.auction_id);
return await response.payload();
}
const Counter = () => {
// component states
const [startTime, setStartTime] = useAtom(startTimeAtom);
const [auctionStatus, setAuctionStatus] = useAtom(auctionStatusAtom);
useEffect(() => {
getAuctionStatus().then((response) => {
setAuctionStatus(response.status);
setStartTime(toLocalDateTime(response.end_time, WpaReactUi.time_zone));
});
}, [auctionStatus]);
//
const handleRenderer = ({ completed, formatted }) => {
if (completed) {
console.log("auction status now is:", auctionStatus);
setTimeout(() => {
if (auctionStatus === 'future') {
getAuctionStatus().then((response) => {
setAuctionStatus(response.status);
});
}
}, 2000)
}
return Object.keys(formatted).map((key) => {
return (
<div key={`${key}`} className={`countDown bordered ${key}-box`}>
<span className={`num item ${key}`}>{formatted[key]}</span>
<span>{key}</span>
</div>
);
});
};
console.log('starttime now:', startTime);
return (
startTime && (
<div className="bidAuctionCounterContainer">
<div className="bidAuctionCounterInner">
<Countdown
key={auctionStatus}
autoStart={true}
id="bidAuctioncounter"
date={startTime}
intervalDelay={0}
precision={3}
renderer={handleRenderer}
/>
</div>
</div>
)
);
};
export default Counter;
You use auctionStatus as a dependency for useEffect.
And when response.status is the same, the auctionStatus doesn't change, so your useEffect won't be called again.
For answering your comment on how to resolve the issue..
I am not sure of your logic but I'll explain by this simple example.
export function App() {
// set state to 'live' by default
const [auctionStatus, setAuctionStatus] = React.useState("live")
React.useEffect(() => {
console.log('hello')
changeState()
}, [auctionStatus])
function changeState() {
// This line won't result in calling your useEffect
// setAuctionStatus("live") // 'hello' will be printed one time only.
// You need to use a state value that won't be similar to the previous one.
setAuctionStatus("inactive") // useEffect will be called and 'hello' will be printed twice.
}
}
You can simply use a flag instead that will keep on changing from true to false like this:
const [flag, setFlag] = React.useState(true)
useEffect(() => {
// ..
}, [flag])
// And in handleRenderer
getAuctionStatus().then((response) => {
setFlag(!flag);
});
Have a look at the following useCountdown hook:
https://codepen.io/AdamMorsi/pen/eYMpxOQ
const DEFAULT_TIME_IN_SECONDS = 60;
const useCountdown = ({ initialCounter, callback }) => {
const _initialCounter = initialCounter ?? DEFAULT_TIME_IN_SECONDS,
[resume, setResume] = useState(0),
[counter, setCounter] = useState(_initialCounter),
initial = useRef(_initialCounter),
intervalRef = useRef(null),
[isPause, setIsPause] = useState(false),
isStopBtnDisabled = counter === 0,
isPauseBtnDisabled = isPause || counter === 0,
isResumeBtnDisabled = !isPause;
const stopCounter = useCallback(() => {
clearInterval(intervalRef.current);
setCounter(0);
setIsPause(false);
}, []);
const startCounter = useCallback(
(seconds = initial.current) => {
intervalRef.current = setInterval(() => {
const newCounter = seconds--;
if (newCounter >= 0) {
setCounter(newCounter);
callback && callback(newCounter);
} else {
stopCounter();
}
}, 1000);
},
[stopCounter]
);
const pauseCounter = () => {
setResume(counter);
setIsPause(true);
clearInterval(intervalRef.current);
};
const resumeCounter = () => {
setResume(0);
setIsPause(false);
};
const resetCounter = useCallback(() => {
if (intervalRef.current) {
stopCounter();
}
setCounter(initial.current);
startCounter(initial.current - 1);
}, [startCounter, stopCounter]);
useEffect(() => {
resetCounter();
}, [resetCounter]);
useEffect(() => {
return () => {
stopCounter();
};
}, [stopCounter]);
return [
counter,
resetCounter,
stopCounter,
pauseCounter,
resumeCounter,
isStopBtnDisabled,
isPauseBtnDisabled,
isResumeBtnDisabled,
];
};

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';

React hooks with useState

I've got the following code:
export default function App() {
const [lastMessageId, setLastMessageId] = useState(0);
const [messages, setMessages] = useState([]);
const addMessage = (body, type) => {
const newMessage = {
id: lastMessageId + 1,
type: type,
body: body,
};
setLastMessageId(newMessage.id)
setMessages([...messages, newMessage]);
console.log("point 1", messages);
return newMessage.id;
}
// remove a message with id
const removeMessage = (id) => {
const filter = messages.filter(m => m.id !== id);
console.log("point 2", filter);
setMessages(filter);
}
// add a new message and then remove it after some seconds
const addMessageWithTimer = (body, type="is-primary", seconds=5) => {
const id = addMessage(body, type);
setTimeout(() => removeMessage(id), seconds*1000);
};
return (
...
);
}
I would like to know why after I setMessages at point 1, when I do console log it doesn't appear to be updated. This turns into a weird behaviour when I call addMessageWithTimer because when it calls removeMessage then it doesn't remove correctly the messages that I expect.
Could you please explain me how to do it?
Just like setState in class-components, the update functions of useState don't immediately update state, they schedule state to be updated.
When you call setMessages it causes react to schedule a new render of App which will execute the App function again, and useState will return the new value of messages.
And if you think about it from a pure JS perspective, messages can't change: it's just a local variable, (a const one, even). Calling a non-local function can't cause a local variable's value to change, JS just doesn't work that way.
#Retsam is correct in his explanation.
I think you would get an issue if you don't use setTimeout in addMessageWithTimer. Isn't it? But for now, it is correct.
If you don't want to give a timer of 5 seconds and still want to keep it running correctly, then give a timer of 0 seconds. It would still work okay.
what weird behavior your seeing?
when I tried your code, I'm able to remove the added message after 5 sec.
import React, { useState } from "react";
import "./styles.css";
export default function App() {
let bodyText = "";
const [lastMessageId, setLastMessageId] = useState(0);
const [messages, setMessages] = useState([]);
const addMessage = (body, type) => {
if (body === "") return;
const newMessage = {
id: lastMessageId + 1,
type: type,
body: body
};
setLastMessageId(newMessage.id);
setMessages([...messages, newMessage]);
bodyText = "";
return newMessage.id;
};
// remove a message with id
const removeMessage = (id) => {
const filter = messages.filter((m) => m.id !== id);
console.log("point 2", filter);
setMessages(filter);
};
// add a new message and then remove it after some seconds
const addMessageWithTimer = (body, type = "is-primary", seconds = 5) => {
const id = addMessage(body, type);
setTimeout(() => removeMessage(id), seconds * 1000);
};
console.log("point 1", messages);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<input onChange={(e) => (bodyText = e.target.value)} />
<button onClick={(e) => addMessage(bodyText, "is-primary")}>
Add messsage
</button>
<button onClick={(e) => addMessageWithTimer(bodyText, "is-primary", 5)}>
Add temp messsage
</button>
{messages.map((message, id) => {
return (
<div key={id}>
<p>
{message.id} {message.body}
</p>
</div>
);
})}
</div>
);
}
#Retsam was very useful with his answer as I was able to understand the problem and find a proper solution.
here is the solution that I've found:
export default function App() {
const [lastMessageId, setLastMessageId] = useState(0);
const [messages, setMessages] = useState([]);
const addMessage = (body, type="is-primary") => {
const newMessage = {
id: lastMessageId + 1,
type: type,
body: body
};
setLastMessageId(newMessage.id)
setMessages([...messages, newMessage]);
return newMessage.id;
}
// delete messages after 5 seconds
useEffect(() => {
if (!messages.length) return;
const timer = setTimeout(() => {
const remainingMessages = [...messages];
remainingMessages.shift();
setMessages(remainingMessages);
}, 5*1000);
return () => clearTimeout(timer);
}, [messages]);
return (
...
);
}

Right way to update state with react socket hooks

I am a beginner to react and I feel this question has been asked several times in the past but I can't see to understand what I am missing to get this working.
I am writing a simple browser based game. I am using socket-io to connect to the server and I am using a custom hook for obtaining state sent by the server
export const useSocketState = (serverUrl: string) => {
const [isConnected, setConnected] = useState(false)
const [socketId, setSocketId] = useState('')
const [gameState, setGameState] = useState(DEFAULT_INITIAL_STATE)
useEffect(() => {
const client = socket.connect(serverUrl)
client.on("connect", () => {
console.log("connected with id " + client.id)
setConnected(true)
setSocketId(client.id)
});
client.on("disconnect", () => {
setConnected(false)
});
client.on("gameStateUpdate", (data: ClientGameState) => {
console.dir("Recieved new state from server" + data);
console.dir("GameState is " + gameState);
setGameState(prevState => {
const newState = data
console.log(" new state is " + newState)
return newState
})
});
}, gameState);
return {gameState, isConnected, socketId}
}
I am using this hook in a component where I am referrring to the gameState returned by this hook.
I am expecting that when server sends a new state my gameState is actually set with a new state. I do see the log statement where a newState is printed when the server returns a new state but I am not seeing it being reflected in the component I am using.
Game component
import React from "react";
import {useSocketState} from "./useSocket";
import {ClientGameState} from "../game/GameState";
export function GameComponent(props: any) {
const {gameState, isConnected, socketId} = useSocketState("http://127.0.0.1:8080");
if (gameState.state === 'created') {
return (
<div>
<p> Game id for this game: {props.location.state.gameId} and playerId: {props.location.state.playerId} </p>
<p>{JSON.stringify(gameState)}</p>
</div>
);
} else {
return (
<div>
<p> Game state is not created </p>
</div>
)
}
}
export const useSocketState = (serverUrl: string) => {
const [isConnected, setConnected] = useState(false)
const [socketId, setSocketId] = useState('')
const [gameState, setGameState] = useState(DEFAULT_INITIAL_STATE);
function socketConnection(){
const client = socket.connect(serverUrl)
client.on("connect", () => {
console.log("connected with id " + client.id)
setConnected(true)
setSocketId(client.id)
});
client.on("disconnect", () => {
setConnected(false)
});
client.on("gameStateUpdate", (data: ClientGameState) => {
console.dir("Recieved new state from server" + data);
console.dir("GameState is " + gameState);
setGameState(prevState => {
const newState = data
console.log(" new state is " + newState)
return newState
})
});
}
useEffect(() => {
socketConnection();
}, []);
return {gameState, isConnected, socketId}
}
When you connect to the socket, you don't need to use useEffect() anymore.
Socket connection needed be made when component is mounted. So, you'll need second parameter of useEffect:
useEffect(() => {
// ... connection
}, []) // run on component mount
You may also use useEffect when some variable changes:
useEffect(() => { // additional useEffect hook
// ... changes being detected
}, [gameState])
How about just passing the data that is returned from the socket to your setGameState hook.
client.on("gameStateUpdate", (data: ClientGameState) => {
setGameState(data);
});

Resources