referencing the latest state in useInterval that uses useRef - reactjs

As Dan Abramov introduced, I wrote an interval using useRef so it does not get created on every render. My code below saves the redux store into local storage every 20 seconds. However, the store inside of setInterval does not get updated which leads to saving the same initial store every 20 seconds.
import { useRef, useState } from "react";
import { useSelector } from "react-redux";
import styles from "./AutoSaveButton.module.scss";
import LOCAL_STORAGE_KEY from "../../../constants/localStorage";
const TWO_SECONDS = 2000;
const TWENTY_SECONDS = 20000;
function AutoSaveButton() {
const store = useSelector((state) => state);
const interval = useRef(null);
const [isAutoSaveOn, setIsAutoSaveOn] = useState(false);
const [isSavedMsgDisplayed, setIsSavedMsgDisplayed] = useState(false);
const toggleAutoSave = () => {
if (isAutoSaveOn) {
setIsAutoSaveOn(false);
clearInterval(interval.current);
return;
}
setIsAutoSaveOn(true);
interval.current = setInterval(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(store)); <-- how to update?
setIsSavedMsgDisplayed(true);
setTimeout(() => {
setIsSavedMsgDisplayed(false);
}, TWO_SECONDS);
}, TWENTY_SECONDS);
};
return (
<span
className={isAutoSaveOn ? styles.active : styles.stale}
onClick={toggleAutoSave}
>
{isAutoSaveOn ? "auto save off" : "auto save on"}
{isAutoSaveOn && (
<span
className={`material-symbols-outlined ${
isSavedMsgDisplayed ? styles["icon-visible"] : styles["icon-greyed"]
}`}
>
done
</span>
)}
</span>
);
}
export default AutoSaveButton;

since closure will always capture the variable store each time it's reinitialized. use [YOUR STORE].getState().[your reducer] instead
const { useRef, useState } = React
const { useSelector,Provider,useDispatch } = ReactRedux
const { configureStore,createSlice } = window.RTK
//import styles from "./AutoSaveButton.module.scss";
const LOCAL_STORAGE_KEY = "XXX"
const slice = createSlice({
name: 'data',
initialState: {
value: 0,
},
reducers: {
setData: (state, action) => {
state.value += action.payload
},
},
})
const mystore=configureStore({
reducer: {
data: slice.reducer
},
})
const TWO_SECONDS = 2000;
const TWENTY_SECONDS = 20000;
function AutoSaveButton() {
const store = useSelector((state) => state.data);
const interval = useRef(null);
const [isAutoSaveOn, setIsAutoSaveOn] = useState(false);
const [isSavedMsgDisplayed, setIsSavedMsgDisplayed] = useState(false);
const toggleAutoSave = (store) => {
if (isAutoSaveOn) {
setIsAutoSaveOn(false);
clearInterval(interval.current);
return;
}
setIsAutoSaveOn(true);
console.log("store outside", store);
interval.current = setInterval(() => {
setIsSavedMsgDisplayed(true);
console.log("store inside", mystore.getState().data);
setTimeout(() => {
setIsSavedMsgDisplayed(false);
}, TWO_SECONDS);
//localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(mystore.getState().data)); //<-- how to update?
}, 1000);
};
return (
<span
className=""
onClick={()=>toggleAutoSave(store)}
>
{isAutoSaveOn ? "auto save off" : "auto save on"}
{isAutoSaveOn && (
<span
className={`material-symbols-outlined `}
>
done
</span>
)}
</span>
);
}
const App=()=>{
return <Provider store={mystore}>
<AutoSaveButton />
<B/>
</Provider>
}
const B=()=>{
const dispatch=useDispatch()
return <button onClick={()=>dispatch(slice.actions.setData(12))}>
setData
</button>
}
ReactDOM.render(<App/>,document.getElementById("App"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/8.0.2/react-redux.min.js"></script>
<script src="https://unpkg.com/#reduxjs/toolkit#1.8.3/dist/redux-toolkit.umd.js"></script>
<div id="App">
</div>
second way watchc store with useEffect and put it to ref
const { useRef, useState, useEffect} = React
const { useSelector,Provider,useDispatch } = ReactRedux
const { configureStore,createSlice } = window.RTK
//import styles from "./AutoSaveButton.module.scss";
const LOCAL_STORAGE_KEY = "XXX"
const slice = createSlice({
name: 'data',
initialState: {
value: 0,
},
reducers: {
setData: (state, action) => {
state.value += action.payload
},
},
})
const mystore=configureStore({
reducer: {
data: slice.reducer
},
})
const TWO_SECONDS = 2000;
const TWENTY_SECONDS = 20000;
function AutoSaveButton() {
const store = useSelector((state) => state.data);
const interval = useRef(null);
const mystore = useRef(store);
useEffect(()=>{
mystore.current=store
},[store])
const [isAutoSaveOn, setIsAutoSaveOn] = useState(false);
const [isSavedMsgDisplayed, setIsSavedMsgDisplayed] = useState(false);
const toggleAutoSave = () => {
if (isAutoSaveOn) {
setIsAutoSaveOn(false);
clearInterval(interval.current);
return;
}
setIsAutoSaveOn(true);
console.log("store outside", store);
interval.current = setInterval(() => {
setIsSavedMsgDisplayed(true);
console.log("store inside", mystore.current);
setTimeout(() => {
setIsSavedMsgDisplayed(false);
}, TWO_SECONDS);
//localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(mystore.current); //<-- how to update?
}, 1000);
};
return (
<span
className=""
onClick={toggleAutoSave}
>
{isAutoSaveOn ? "auto save off" : "auto save on"}
{isAutoSaveOn && (
<span
className={`material-symbols-outlined `}
>
done
</span>
)}
</span>
);
}
const App=()=>{
return <Provider store={mystore}>
<AutoSaveButton />
<B/>
</Provider>
}
const B=()=>{
const dispatch=useDispatch()
return <button onClick={()=>dispatch(slice.actions.setData(12))}>
setData
</button>
}
ReactDOM.render(<App/>,document.getElementById("App"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/8.0.2/react-redux.min.js"></script>
<script src="https://unpkg.com/#reduxjs/toolkit#1.8.3/dist/redux-toolkit.umd.js"></script>
<div id="App">
</div>

I think I found an answer myself, but if any of you find this dumb, please leave a comment. I am open to discussions!
import { useEffect, useRef, useState } from "react";
import { useSelector } from "react-redux";
import styles from "./AutoSaveButton.module.scss";
import LOCAL_STORAGE_KEY from "../../../constants/localStorage";
const TWO_SECONDS = 2000;
const TWENTY_SECONDS = 5000;
function AutoSaveButton() {
const store = useSelector((state) => state);
const interval = useRef(null);
const [isAutoSaveOn, setIsAutoSaveOn] = useState(false);
const [isSavedMsgDisplayed, setIsSavedMsgDisplayed] = useState(false);
const toggleAutoSave = () => {
if (isAutoSaveOn) {
setIsAutoSaveOn(false);
clearInterval(interval.current);
return;
}
setIsAutoSaveOn(true);
interval.current = setInterval(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(store));
setIsSavedMsgDisplayed(true);
setTimeout(() => {
setIsSavedMsgDisplayed(false);
}, TWO_SECONDS);
}, TWENTY_SECONDS);
};
/* I added below useEffect and got desired result! */
useEffect(() => {
if (!interval.current) return;
clearInterval(interval.current);
interval.current = setInterval(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(store));
setIsSavedMsgDisplayed(true);
setTimeout(() => {
setIsSavedMsgDisplayed(false);
}, TWO_SECONDS);
}, TWENTY_SECONDS);
}, [store]);
return (
<span
className={isAutoSaveOn ? styles.active : styles.stale}
onClick={toggleAutoSave}
>
{isAutoSaveOn ? "auto save off" : "auto save on"}
{isAutoSaveOn && (
<span
className={`material-symbols-outlined ${
isSavedMsgDisplayed ? styles["icon-visible"] : styles["icon-greyed"]
}`}
>
done
</span>
)}
</span>
);
}
export default AutoSaveButton;

Related

TypeScript Reset countdown timer when all questions have been answered or timer hits zero

I followed a tutorial online for a react quiz using typescript. I have the countdown/timer working (with help from a friend) but now I am looking to reset it back to the original countdown value when either the last question has been answered or when the countdown timer has reached zero seconds.
App.tsx
import React, { useEffect, useState } from 'react';
// Components
import QuestionCard from './components/QuestionCard';
// Types
import { QuestionState } from './API';
import Timer, { useTimer } from './components/Timer';
import StartButton from './components/StartButton';
export type AnswerObject = {
question: string;
answer: string;
correct: boolean;
correctAnswer: string;
};
const TOTAL_QUESTIONS = 10;
const TIME_ALLOWED = 20;
const App = () => {
const [loading, setLoading] = useState(false);
const [questions, setQuestions] = useState<QuestionState[]>([]);
const [number, setNumber] = useState(0);
const [userAnswers, setUserAnswers] = useState<AnswerObject[]>([]);
const [score, setScore] = useState(0);
const [gameOver, setGameOver] = useState(true);
const { timeRemaining, toggle, reset, isActive } = useTimer(TIME_ALLOWED);
const checkAnswer = (e: React.MouseEvent<HTMLButtonElement>) => {
if (!gameOver) {
//Users answer
const answer = e.currentTarget.value;
// Check answer against correct answer
const correct = questions[number].correct_answer === answer;
// Add score if answer is correct
if (correct) setScore((prev) => prev + 1);
// Save answer in the array for user answers
const answerObject = {
question: questions[number].question,
answer,
correct,
correctAnswer: questions[number].correct_answer,
};
setUserAnswers((prev) => [...prev, answerObject]);
console.log({ answerObject });
}
};
const nextQuestion = () => {
// Move on to the next question if not the last question
const nextQuestion = number + 1;
if (nextQuestion === TOTAL_QUESTIONS) {
setGameOver(true);
} else {
setNumber(nextQuestion);
}
};
useEffect(() => {
// If the timer reaches 0, the game is over
if (timeRemaining <= 0) {
setGameOver(true);
}
}, [timeRemaining]);
return (
<div className="App">
<h1>REACT QUIZ</h1>
{(gameOver || userAnswers.length === TOTAL_QUESTIONS) && (
<StartButton
setLoading={setLoading}
setQuestions={setQuestions}
setUserAnswers={setUserAnswers}
setNumber={setNumber}
setScore={setScore}
setGameOver={setGameOver}
toggleTimer={toggle}
/>
)}
{!gameOver ? <p className="score">Score: {score}</p> : null}
{loading && <p>Loading Questions...</p>}
{!loading && !gameOver && (
<QuestionCard
questionNr={number + 1}
totalQuestions={TOTAL_QUESTIONS}
question={questions[number].question}
answers={questions[number].answers}
userAnswer={userAnswers ? userAnswers[number] : undefined}
callback={checkAnswer}
/>
)}
{!gameOver &&
!loading &&
userAnswers.length === number + 1 &&
number !== TOTAL_QUESTIONS - 1 ? (
<button className="next" onClick={nextQuestion}>
Next Question
</button>
) : null}
<Timer
timeRemaining={timeRemaining}
isActive={isActive}
toggle={toggle}
reset={reset}
/>
</div>
);
};
export default App;
Timer.tsx
import React, { useMemo, useState, useEffect } from 'react';
export const useTimer = (timeAllowed: number) => {
const [seconds, setSeconds] = useState(0);
const [isActive, setIsActive] = useState(false);
const timeRemaining = useMemo(
() => timeAllowed - seconds,
[timeAllowed, seconds]
);
function toggle() {
setIsActive(!isActive);
}
function reset() {
setSeconds(0);
setIsActive(false);
}
useEffect(() => {
let interval: any = null;
if (isActive) {
interval = setInterval(() => {
setSeconds((seconds) => seconds + 1);
}, 1000);
} else if (!isActive && seconds !== 0) {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [isActive, seconds]);
return { timeRemaining, toggle, reset, isActive };
};
const Timer = ({ timeRemaining }: any) => {
return <p className="time">{timeRemaining}s</p>;
};
export default Timer;
Startbutton.tsx
import React, { useState } from 'react';
import { Difficulty, fetchQuizQuestions } from '../API';
export type AnswerObject = {
question: string;
answer: string;
correct: boolean;
correctAnswer: string;
};
const TOTAL_QUESTIONS = 10;
interface IStartButton {
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
setQuestions: React.Dispatch<React.SetStateAction<any>>;
setUserAnswers: React.Dispatch<React.SetStateAction<any>>;
setNumber: React.Dispatch<React.SetStateAction<any>>;
setScore: React.Dispatch<React.SetStateAction<any>>;
setGameOver: React.Dispatch<React.SetStateAction<boolean>>;
toggleTimer: () => void;
}
const StartButton = ({
setLoading,
setQuestions,
setUserAnswers,
setNumber,
setScore,
setGameOver,
toggleTimer,
}: IStartButton) => {
const startTrivia = async () => {
setLoading(true);
setGameOver(false);
const newQuestions = await fetchQuizQuestions(
TOTAL_QUESTIONS,
Difficulty.EASY
);
toggleTimer();
setQuestions(newQuestions);
setScore(0);
setUserAnswers([]);
setNumber(0);
setLoading(false);
};
return (
<button className="start" onClick={startTrivia}>
Start
</button>
);
};
export default StartButton;
I thought about adding to the nextQuestion function in the if else statement that the Timer function would reset, but I wasn't too sure because of the useEffect in timer.tsx. So far when all the questions have been answered the timer keeps going until you hit the start button again. Thanks for the help in advance.

Not Rendering Card - React

I'm new to React, and I would like to know if someone can help me?
I'm trying to use useEffect and State to manipulate the API.
But the cards are not rendering.
Sometimes all the cards are rendering, other times not.. and they always come on a different order even after sorting them :( Can you help me?
App.js
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, useEffect } from "react";
import PlayerList from "./PlayerList";
import axios from "axios";
function App() {
const Team = [
...
];
const Team2 = [
...
];
const Team3 = [
...
];
const teamForLoop = [Team, Team2, Team3];
const [allPlayers, setAllPlayers] = useState([]);
const [team, setTeam] = useState([]);
const [allTeams] = useState(teamForLoop);
const [loading, setLoading] = useState(true);
useEffect(() => {
const playerInfo = async () => {
setLoading(true);
allTeams.map(async (teamArray) => {
setTeam([]);
teamArray.map(async (player) => {
let playerName = player.split(" ");
const result = await axios.get(
`https://www.thesportsdb.com/api/v1/json/2/searchplayers.php?p=${playerName[0]}%20${playerName[1]}`
);
if (result.data.player === null) {
setTeam((state) => {
return [...state];
});
} else {
setTeam((state) => {
return [...state, result.data.player[0]];
});
}
});
setAllPlayers(team);
});
setLoading(false);
};
playerInfo();
}, [allTeams]);
if (loading) return "...Loading...";
return (
<>
<PlayerList allPlayers={allPlayers} />
</>
);
}
export default App;
PlayerList.js
import React from "react";
export default function PlayerList({ allPlayers }) {
const myData = []
.concat(allPlayers)
.sort((a, b) => (a.strNumber > b.strNumber ? 1 : -1))
.sort((a, b) => (a.idTeam !== b.idTeam ? 1 : -1));
return (
<div>
{myData.map((player, index) => (
<div key={index}>
<div className="playerCard">
<img
className="playerImage"
src={player.strCutout}
alt={`${player.strPlayer}`}
/>
<h1 className="playerName">{player.strPlayer}</h1>
<h2 className="playerNumber">{player.strNumber}</h2>
</div>
</div>
))}
</div>
);
}
Codesandbox link:
"https://codesandbox.io/s/busy-orla-v872kt?file=/src/App.js"

useEffect dosn't save data in localstorage

I have a simple app, sorta for chat purpuses. I fetch data from static file in json format. So this app shows all the messages from that file but also I want to edit the messeges, delete them and add via local storage. For that I used useEffect, but after refresh all the changes I do disappear.
This is my component:
export const WorkChat = (props) => {
const [messageValue, setMessageValue] = useState('');
const [edit, setEdit] = useState(null);
const [editmessageValue, setMessageEditValue] = useState('')
const submitMessage = () => {
const newMessage = {
id: Math.floor(Math.random() * 10000),
message: messageValue
}
props.addMessage(newMessage);
setMessageValue('')
}
const removeMsg = (id) => {
props.deleteMessage(id)
}
const goToEditMode = (message) => {
setEdit(message.id);
setMessageEditValue(message.message)
}
const saveChanges = (id) => {
const newMessagesArray = props.messages.map(m => {
if(m.id === id){
m.message = editmessageValue
}
return m
})
props.updateMessage(newMessagesArray);
setEdit(null)
}
useEffect(()=> {
let data = localStorage.getItem('work-messages');
if(data){
props.setMessages(JSON.parse(data))
}
}, []);
useEffect(()=> {
localStorage.setItem('work-messages', JSON.stringify(props.messages))
},[props.messages])
return (
<div className={s.workChatContainer}>
<input className={s.workInput} placeholder='Enter work message...' onChange={(e)=> setMessageValue(e.target.value)} value={messageValue}/>
<button className={`${s.btn} ${s.sendBtn}`} onClick={()=>submitMessage()}><SendIcon style={{fontSize: 20}}/></button>
<div>
{props.messages.map(m => (
<div key={m.id} className={s.messages}>
{edit !== m.id ? <div>
<span className={s.message}>{m.message}</span>
<button className={`${s.btn} ${s.deleteBtn}`} onClick={()=> removeMsg(m.id)}><DeleteOutlineIcon style={{fontSize: 15}}/></button>
<button className={`${s.btn} ${s.editBtn}`} onClick={()=> goToEditMode(m)}><EditIcon style={{fontSize: 15}}/></button>
</div>
:
<form>
<input className={s.editInput} value={editmessageValue} onChange={(e)=> setMessageEditValue(e.target.value)}/>
<button className={`${s.btn} ${s.saveBtn}`} onClick={()=> saveChanges(m.id)}><BeenhereIcon style={{fontSize: 15}}/></button>
</form>
}
</div>
))}
</div>
</div>
)
}
Just in case, this is my container component:
import { connect } from "react-redux"
import { setFloodMessagesAC, addFloodMessageAC, deleteFloodMessageAC, upadateMessageAC } from "../../redux/flood-reducer"
import { FloodChat } from "./FloodChat"
import { useEffect } from 'react'
import data from '../../StaticState/dataForFlood.json'
const FloodChatApiContainer = (props) => {
useEffect(()=> {
props.setFloodMessages(data)
}, [])
return <FloodChat messages={props.messages}
setFloodMessages={props.setFloodMessages}
addFloodMessage={props.addFloodMessage}
deleteFloodMessage={props.deleteFloodMessage}
upadateMessage={props.upadateMessage}
/>
}
const mapStateToProps = (state) => ({
messages: state.flood.messages
})
export const FloodChatContainer = connect(mapStateToProps, {
setFloodMessages: setFloodMessagesAC,
addFloodMessage: addFloodMessageAC,
deleteFloodMessage: deleteFloodMessageAC,
upadateMessage: upadateMessageAC
})(FloodChatApiContainer)
Why useEffect doesn't work? It seems to me like it should, but it doesnt.
I figured it out. Since I use data from static file, I need to implement functions that get/set data from/to local storage right where I import it which is container component. Once I put those useEffect functions in container component it works perfectly well.
const FloodChatApiContainer = (props) => {
useEffect(()=> {
props.setFloodMessages(data)
}, [])
useEffect(()=> {
let data = JSON.parse(localStorage.getItem('flood-messages'));
if(data){
props.setFloodMessages(data)
}
console.log('get')
}, [])
useEffect(() => {
localStorage.setItem('flood-messages', JSON.stringify(props.messages));
console.log('set')
}, [props.messages]);
return <FloodChat messages={props.messages}
setFloodMessages={props.setFloodMessages}
addFloodMessage={props.addFloodMessage}
deleteFloodMessage={props.deleteFloodMessage}
upadateMessage={props.upadateMessage}
/>
}
const mapStateToProps = (state) => ({
messages: state.flood.messages
})
export const FloodChatContainer = connect(mapStateToProps, {
setFloodMessages: setFloodMessagesAC,
addFloodMessage: addFloodMessageAC,
deleteFloodMessage: deleteFloodMessageAC,
upadateMessage: upadateMessageAC
})(FloodChatApiContainer)

React Redux state cannot rerender component and state is UNDEFINED

There is some issues with state in Redux. I want to change color of a component with state update. But the state in TargetList.js has error: TypeError: Cannot read property 'includes' of undefined.
The Reducer file is:
import {
SENSOR_HI,
SENSOR_UNHI,
} from "redux/actionTypes";
const initialState = {
title: "",
text: "",
detailedList: "hide",
};
const Reducer = (state = initialState, action) => {
switch (action.type) {
case SENSOR_HI:
return {
...state,
sensorarray: action.data,
};
case SENSOR_UNHI:
return {
...state,
sensorarray: [],
};
case FILTER_TAGS_CHANGED:
return {
...state,
filterTags: action.data,
};
default:
return state;
}
};
export default Reducer;
And the TargetList.js is:
import "assets/scss/global.scss";
import moment from "jalali-moment";
import React, { useState, useEffect, useRef, memo } from "react";
import { OverlayTrigger, Tooltip } from "react-bootstrap";
import { GiMovementSensor, GiRadarSweep } from "react-icons/gi";
import { MdDescription, MdSignalWifiOff,MdNetworkWifi } from "react-icons/md";
import { connect } from "react-redux";
import ToggleButton from "react-switch";
import socketConn from "sockets";
import "./Leaflet.scss";
import classes from "./TargetsList.module.scss";
import { targetTypes, targetsIconsHandlerImage } from "helpers/icons";
import { BackButton } from "assets/js/Icons";
import ReactDOMServer from "react-dom/server";
let TEMP_TARGETS = [];
let counter = 0;
let now = new Date();
const TargetsList = memo((props) => {
const [sensorFiltersSelected, setSensorFiltersSelected] = useState([]);
const [sensorFiltersSelectedIds, setSensorFiltersSelectedIds] = useState([]);
const [sensorSelected, setSensorSelected] = useState(null);
const [targetFiltersSelected, setTargetFiltersSelected] = useState([]);
const [hi, sethi] = useState(false);
let filterType = (sensors) => {
if (sensorFiltersSelected.length == 0) {
return sensors;
} else {
let ids = sensorFiltersSelected.map((s) => s["_id"]);
return sensors.filter((s) => ids.includes(s.sensorType["_id"]));
}
};
let filterTextSensors = (sensors) => {
return sensors.filter((s) => s.latinName.includes(searchInput));
};
let toggleFilter = (sensor) => {
let filters = sensorFiltersSelected;
let ids = filters.map((f) => f["_id"]);
if (filters.length && ids.includes(sensor["_id"])) {
filters = filters.filter((f) => f["_id"] != sensor["_id"]);
} else {
filters.push(sensor);
}
props.dispatch({
type: "FILTER_TAGS_CHANGED",
data: [...filters],
});
setSensorFiltersSelected(filters);
setSensorFiltersSelectedIds(filters.map(s=>s.latinName))
props.dispatch({
type: "SENSOR_HI",
data: [filters.map(s=>s.latinName)],
});
};
return (
<div>
<div
className="container"
style={{
padding: 0,
}}
>
<div className="rowx my-2 flex-wrap">
{props.sensors && props.sensors.length
? filterTextSensors(props.sensors).map(
(s, sensorTypeIndex) => (
<div
key={s._id}
/////////////////////////////////////// Here is Error ///////////////////////////////////////////////
className={`cursor-pointer badge badge-pill m-1 ${props.sensorarray.includes(s.latinName) ? `badge-success` : `badge-light`}`}
onClick={() => {
showLogsSensor(s)
toggleFilter(s);
}}
>
{s.persianName}
</div>
)
)
: ""}
</div>
});
const mapStateToProps = (state) => {
return {
sensorarray: state.sensorarray,
};
};
export default connect(mapStateToProps)(TargetsList);
And the Drawer.js is change state that should color change in TargetList.js.
Drawer.js:
import React from "react";
import clsx from "clsx";
import {makeStyles} from "#material-ui/core/styles";
import Drawer from "#material-ui/core/Drawer";
import {connect} from "react-redux";
import "assets/scss/global.scss";
import "./style.scss";
import socketConn from "sockets";
import moment from "jalali-moment";
import {MdSave} from "react-icons/md";
import axios from "config.js";
const useStyles = makeStyles({
fullList: {
width: "auto",
},
});
function BottomDrawer(props) {
const classes = useStyles();
const [tab, setTab] = React.useState("sensors");
const [state, setState] = React.useState({bottom: false});
const [type, setType] = React.useState(null);
const [info, setInfo] = React.useState(null);
const [logs, setLogs] = React.useState([]);
const toggleDrawer = (anchor, open) => {
setState({
...state,
[anchor]: open,
});
// stop receiving target logs
targetsLogAPIHandler(false);
};
const sensorunhi = () => {
props.dispatch({
type: "SENSOR_UNHI",
});
};
let setLogSocket = (type, info) => {
setLogs([]);
if (type === "target") {
targetsLogAPIHandler(true, info);
}
if (type === "sensor") {
sensorsLogAPIHandler(info);
}
};
// handler for API request to NodeJS to get logs of specific target (! ! ! requestMode => {true: when you want to START coming logs | false: when you want to STOP coming logs})
const targetsLogAPIHandler = async (requestMode, info) => {
try {
const userId = localStorage.getItem('user_id');
socketConn().removeAllListeners("show_specific_target_log");
const {data} = await axios.post(`/targets/log/${userId}`, {
requestMode,
unique_id: info ? info.unique_id : undefined
});
socketConn().on("show_specific_target_log", targetLogs => {
setLogs((prev) => {
return [targetLogs, ...prev].filter((s, index) => index < 500);
});
});
} catch (err) {
console.log(err);
}
};
// handler for API request to NodeJS to get logs of specific sensor
const sensorsLogAPIHandler = async info => {
try {
socketConn().removeAllListeners("sensor_moment_log");
let userId = localStorage.getItem("user_id");
const logs = await axios.post(`sensors/sensors/logs`, {topic: info.latinName, userId});
socketConn().on("sensor_moment_log", (data) => {
let enc = new TextDecoder("utf-8");
let decoded = enc.decode(data.result.content);
decoded = JSON.parse(decoded);
setLogs((prev) => {
return [decoded, ...prev].filter((s, index) => index < 500);
});
});
socketConn().on("sensor_moment_log_error", (data) => {
console.log(data);
});
} catch (err) {
console.log(err);
}
};
React.useEffect(() => {
setTab((prev) => {
if (prev != props.selectedTab) {
socketConn().removeAllListeners("sensor_moment_log");
socketConn().removeAllListeners("show_specific_target_log");
setLogs([]);
return props.selectedTab;
}
return prev;
});
}, [props.selectedTab]);
React.useEffect(() => {
if (props.showLogs && state["bottom"] == false) {
toggleDrawer("bottom", true);
}
}, [props.showLogs]);
React.useEffect(() => {
if (props.logInfo && props.logType) {
if (info != null) {
if (
type != props.logType ||
info.Identification + info.sensorName !=
props.logInfo.Identification + props.logInfo.sensorName
) {
setType(props.logType);
setInfo(props.logInfo);
setLogSocket(props.logType, props.logInfo);
}
} else {
setType(props.logType);
setInfo(props.logInfo);
setLogSocket(props.logType, props.logInfo);
}
}
}, [props.logInfo, props.logType]);
const list = anchor => (
<div className={clsx(classes.list, {[classes.fullList]: anchor === "bottom",})} role="presentation">
<div className="log-container p-4">
<React.Fragment>
<div className="rowx log-headers mb-3">
<div className="colx log-header font-weight-bold">نام سنسور</div>
<div className="colx log-header font-weight-bold">نوع هدف</div>
<div className="colx log-header font-weight-bold">طول</div>
<div className="colx log-header font-weight-bold">عرض</div>
<div className="colx log-header font-weight-bold">ارتفاع</div>
</div>
<div className="log-elements">
{logs &&
logs.length &&
logs.map((log, index) => (
<div className="rowx log-element mb-2" key={index}>
<div className="colx log-text">{log && log[0] && log[0].sensorName}</div>
<div className="colx log-text">{log && log[0] && log[0].sensorType}</div>
<div className="colx log-text">{log && log[0] && log[0].longitude}</div>
<div className="colx log-text">{log && log[0] && log[0].latitude}</div>
<div className="colx log-text">{log && log[0] && log[0].altitude}</div>
</div>
))}
</div>
</React.Fragment>
)}
</div>
</div>
);
return (
<div>
<React.Fragment>
<Drawer
classes={{root: "drawer-root"}}
ModalProps={{hideBackdrop: true}}
anchor="bottom"
open={state["bottom"]}
onClose={() => {
toggleDrawer("bottom", false);
sensorunhi(); /////////////////// CHANGE REDUX STATE
}}
>
{list("bottom")}
</Drawer>
</React.Fragment>
</div>
);
}
const mapStateToProps = (state) => {
return {
sensorarray: state.sensorarray,
};
};
export default connect(mapStateToProps)(BottomDrawer);
In other word, I want to change the color component TargetList.js by change the state in Drawer.js.

React State returning out of sync data

I have the below component which is using React hooks:
import React, {useState} from 'react';
// import components
import TagManagementRow from './TagManagementRow';
const TagManagementList = (props) => {
const [tagData, setTagData] = useState(props.data);
const deleteAction = (id) => {
// Call to backend to delete tag
const currentData = [];
for( var i = 0; i <= tagData.length; i++){
if(i < tagData.length && tagData[i].id !== id) {
currentData.push(tagData[i]);
}
if(i === tagData.length) setTagData(currentData);
};
};
return (
<ul className="tagManagement">
{tagData.map( (tag,i) => {
return <TagManagementRow name={tag.name} key={i} id={tag.id} delete={() => deleteAction(tag.id)} />
})}
</ul>
);
}
export default TagManagementList;
It renders 4 TagManagementRow child components, each have a delete button. When I click the delete button, everything looks good if I log out the changed state to the console, however, in the actual browser the last item in the list is removed. I feel like its some kind of render/timing issue but I can't seem to figure it out. Any assistance from those who better understand hooks would be greatly appreciated.
By the way, here is the code for the TagManagementRow component:
import React, { useState } from 'react';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
const TagManagementRow = (props) => {
const [editTag, setEdit] = useState(false);
const [tagName, setTagName] = useState(props.name);
const [tempName, setTempName] = useState('');
const handleEdit = (e) => {
setTempName(e.target.value);
};
const switchToEdit = () => {
setEdit(!editTag);
}
const saveEdit = () => {
setTagName(tempName);
setTempName('');
switchToEdit();
}
return (
<li>
<span>
{tagName}
<FontAwesomeIcon icon={["fas","pen"]} onClick={switchToEdit} />
</span>
<span>
<FontAwesomeIcon icon={["fas","trash-alt"]} onClick={props.delete} />
</span>
</li>
);
}
export default TagManagementRow;
Instead of updating the state inside the loop, you could use filter to filter out the object with the matching id.
Also make sure you use tag.id as key instead of the array index, since that will change when you remove an element.
const { useState } = React;
const TagManagementList = props => {
const [tagData, setTagData] = useState(props.data);
const deleteAction = id => {
setTagData(prevTagData => prevTagData.filter(tag => tag.id !== id));
};
return (
<ul className="tagManagement">
{tagData.map((tag, i) => {
return (
<TagManagementRow
name={tag.name}
key={tag.id}
id={tag.id}
delete={() => deleteAction(tag.id)}
/>
);
})}
</ul>
);
};
const TagManagementRow = props => {
const [editTag, setEdit] = useState(false);
const [tagName, setTagName] = useState(props.name);
const [tempName, setTempName] = useState("");
const handleEdit = e => {
setTempName(e.target.value);
};
const switchToEdit = () => {
setEdit(!editTag);
};
const saveEdit = () => {
setTagName(tempName);
setTempName("");
switchToEdit();
};
return (
<li>
{tagName}
<button onClick={props.delete}>Delete</button>
</li>
);
};
ReactDOM.render(
<TagManagementList data={[{ id: 1, name: "foo" }, { id: 2, name: "bar" }]} />,
document.getElementById("root")
);
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>

Resources