This question already has answers here:
Find object having maximum value for the `id` property in an array of objects
(7 answers)
Closed 2 days ago.
I want to display in the UI only one single record, which is the most recently generated one (that has the highest ID value). Right now I have all the JSON data displayed in the UI but I want to limit that to a single extract.
Data
function PriorityProgressTrim({asms_number}) {
const [checkForRecords, setCheckForRecords] = useState(true);
const [pprecords, setPprecords] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
console.log(asms_number)
// axios(`https://besterdev-api.apps.pcfepg3mi.gm.com/api/v1/pprecords/latest/${asms_number}`).then((response) => {
axios(`http://localhost:8000/api/v1/pprecords/latest/${asms_number}`).then((response) => {
console.log (response.data)
setPprecords(response.data); setError(null); }).catch(setError);}, [checkForRecords]);
if (error) return <p>An error occurred</p>
return (
<div>
{pprecords.map(({id, date, pprecord, asmsNumber}) => (
<Box component="form" sx={{ '& .MuiTextField-root': { m: 0.5, width: '80ch' }, }}>
<div>
<TextField error className='Font' fullWidth id="outlined-multiline-static" size="small" variant="outlined" label={date} multiline rows={5} value={pprecord} />
</div>
</Box>)).reverse()}
</div>);}
export default PriorityProgressTrim;
As you receive a string, you need first to parse to a JSON object:
let data = JSON.parse(response.data);
Then you can sort the elements inside the resulted JSON array, and extract the last one (that with the highest ID):
let lastRecord = data.sort((a, b) => a.id - b.id).pop();
setPprecord(lastRecord);
Here is the complete code:
function PriorityProgressTrim({asms_number}) {
const [checkForRecords, setCheckForRecords] = useState(true);
const [pprecord, setPprecord] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
console.log(asms_number)
// axios(`https://besterdev-api.apps.pcfepg3mi.gm.com/api/v1/pprecords/latest/${asms_number}`).then((response) => {
axios(`http://localhost:8000/api/v1/pprecords/latest/${asms_number}`).then((response) => {
console.log (response.data)
let data = JSON.parse(response.data);
let lastRecord = data.sort((a, b) => a.id - b.id).pop();
setPprecord(lastRecord); setError(null); }).catch(setError);}, [checkForRecords]);
if (error) return <p>An error occurred</p>
return (
<div>
<Box component="form" sx={{ '& .MuiTextField-root': { m: 0.5, width: '80ch' }, }}>
<div>
<TextField error className='Font' fullWidth id="outlined-multiline-static" size="small" variant="outlined" label={date} multiline rows={5} value={pprecord} />
</div>
</Box>
</div>);}
export default PriorityProgressTrim;
Related
I have a specific component that renders twice. Basically, I am generating two random numbers and displaying them on the screen and for a split second, it shows undefined for both then shows the actual numbers. To test it further I did a simple console.log and it indeed logs it twice. So my question splits into two - 1. Why does the typography shows undefined for a split second before rendering? 2 - Why does it render twice?
Here's the related code:
Beginnging.js - this a counter that counts down from 3 and fires an RTK action, setting gameStart true.
function Beginning() {
const [count, setCount] = useState(3);
const [message, setMessage] = useState("");
const dispatch = useDispatch();
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
const handleCount = () => {
if (countRef.current === 1) {
return setMessage("GO");
}
return setCount((count) => count - 1);
};
useEffect(() => {
const interval = setInterval(() => {
handleCount();
}, 1000);
if (message==='GO') {
setTimeout(() => {
dispatch(start());
}, 1000);
}
return () => clearInterval(interval);
}, [count, message]);
return (
<>
<Typography variant="h1" fontStyle={'Poppins'} fontSize={36}>GET READY...</Typography>
<Typography fontSize={48} >{count}</Typography>
<Typography fontSize={60} >{message}</Typography>
</>
);
}
export default Beginning;
AdditionMain.js - based on gameStart, this is where I render Beginning.js, once it counts down, MainInput is rendered.
const AdditionMain = () => {
const gameStart = useSelector((state) => state.game.isStarted);
const operation = "+";
const calculation = generateNumbersAndResults().addition;
if (!gameStart){
return <Beginning/>
}
return (
<>
<MainInput operation={operation} calculation={calculation} />
</>
);
};
export default AdditionMain;
MainInput.js - the component in question.
const MainInput = ({ operation, calculation }) => {
const [enteredValue, setEnteredValue] = useState("");
const [correctValue, setCorrectValue] = useState(false);
const [calculatedNums, setCalculatedNums] = useState({});
const [isIncorrect, setIsIncorrect] = useState(false);
const [generateNewNumbers, setGenerateNewNumbers] = useState(false);
const [haveToEnterAnswer, setHaveToEnterAnswer] = useState(false);
const dispatch = useDispatch();
const seconds = useSelector((state) => state.game.seconds);
const points = useSelector((state) => state.game.points);
const lives = useSelector((state) => state.game.lives);
const timerValid = seconds > 0;
const newChallenge = () => {
setIsIncorrect(false);
setHaveToEnterAnswer(false);
dispatch(restart());
};
const handleCount = () => {
dispatch(loseTime());
};
useEffect(() => {
let interval;
if (timerValid) {
interval = setInterval(() => {
handleCount();
}, 1000);
}
return () => {
console.log("first");
clearInterval(interval);
};
}, [timerValid]);
useEffect(() => {
setCalculatedNums(calculation());
setGenerateNewNumbers(false);
setCorrectValue(false);
setEnteredValue("");
}, [generateNewNumbers]);
const submitHandler = () => {
if (correctValue) {
setGenerateNewNumbers(true);
dispatch(gainPoints());
dispatch(gainTime());
}
if (+enteredValue === calculatedNums.result) {
setCorrectValue(true);
} else if (enteredValue.length === 0) {
setHaveToEnterAnswer(true);
} else {
setIsIncorrect(true);
dispatch(loseLife());
}
};
const inputValueHandler = (value) => {
setIsIncorrect(false);
setHaveToEnterAnswer(false);
setEnteredValue(value);
};
const submitOrTryNewOne = () => {
return correctValue ? "Try new one" : "Submit";
};
return (
<>
<Navbar />
{console.log("hello")}
{seconds && lives > 0 ? (
<>
<GameInfo></GameInfo>
<Container component="main" maxWidth="xs">
<Box
sx={{
marginTop: 8,
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Typography>
Fill in the box to make the equation true.
</Typography>
<Typography fontSize={28}>
{operation !== "/"
? `${calculatedNums.number1} ${operation} ${calculatedNums.number2}`
: `${calculatedNums.number2} ${operation} ${calculatedNums.number1}`}{" "}
=
</Typography>
<TextField
inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }}
type="number"
name="sum"
id="outlined-basic"
label=""
variant="outlined"
onChange={(event) => {
inputValueHandler(event.target.value);
}}
disabled={correctValue}
value={enteredValue}
></TextField>
{haveToEnterAnswer && enterAnswer}
{correctValue && correctAnswer}
{isIncorrect && wrongAnswer}
<Button
type="button"
sx={{ marginTop: 1 }}
onClick={() => submitHandler()}
variant="outlined"
>
{isIncorrect ? "Try again!" : submitOrTryNewOne()}
</Button>
</Box>
</Container>
</>
) : (
<>
<Typography>GAME OVER</Typography>
<Typography>Final Score: {points}</Typography>
<Button onClick={newChallenge}>New Challenge</Button>
</>
)}
</>
);
};
export default MainInput;
PS: I'm trying to figure out how to get this running on Codesandbox
The problem is that you render the page before the calculaed nums are established.
Try this instead. It will prevent the element from displaying until nums are generated.
const [calculatedNums, setCalculatedNums] = useState(null);
// This is down in your render area. Only render once calculatedNums are non-null.
{ calculatedNums &&
<Typography fontSize={28}>
operation !== "/"
? `${calculatedNums.number1} ${operation} ${calculatedNums.number2}`
: `${calculatedNums.number2} ${operation} ${calculatedNums.number1}`}{" "}
=
</Typography>
}
In addition, you are probably generating your numbers twice, because you will generate new numbers on initial load, but then when you hit setGenerateNewNumbers(true); it will trigger a new calculation, which will then set the generatedNewNumbers to false, which will call the calc again, since it triggers whenever that state changes. It stops after that because it tries to set itself to false again and doesn't change.
You are changing a dependency value within the hook itself, causing it to run again. Without looking a lot more into your program flow, a really hacky way of fixing that would just be to wrap your useEffect operation inside an if check:
useEffect(()=>{
if (generateNewNumbers === true) {
//All your stuff here.
}
}, [generateNewNumbers]
That way, it won't run again when you set it to false.
I am getting data which is an array of objects from the store. In useState I want to set an array of data.length size all set to false initially. But when I set the value it returns an empty array [] for each of the state variable I set. I also tried updating the state in the useeffect but nothing works. I am unable to figure out what is the issue here.
function Datatable() {
let data = useSelector((state) => state.dish);
console.log(data.length)
const [clicked1, setClicked1] = useState(new Array(data.length).fill(false));
const [clicked2, setClicked2] = useState(new Array(data.length).fill(false));
const [clicked3, setClicked3] = useState(new Array(data.length).fill(false));
const dispatch = useDispatch();
function setAllStates() {
setClicked1(new Array(data.length).fill(false));
setClicked2(new Array(data.length).fill(false));
setClicked3(new Array(data.length).fill(false));
}
useEffect(() => {
setAllStates();
}, []);
image of console the data
Here is my jsx where i am creating the table
<TableBody>
{data.map((row, index) => (
<TableRow
key={row.id}
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
>
<TableCell component="th" scope="row">
<img width={150} height={100} src={row.image} />
</TableCell>
<TableCell align="right">{row.dishName}</TableCell>
<TableCell align="right">
{clicked1[index] == false ? (
<>
<Button
onClick={() => handleOnclick("Rank 1", index, row)}
>
Rank 1
</Button>
</>
) : (
<Typography>Selected</Typography>
)}
</TableCell>
Here is the image of the table
in console data.length is showing 0 two times and then 30. I am populating the data in the reducers in its parent component. But still all the arrays are undefined. And in the table all i am showing in the data table are undefined. (Note: I am creating a table of length data.length. data.image, data.description are showing in the table only the buttons that are showing only when clicked1[index] == false are not defined.
Unless data.length is 0, this gotta work.
function Datatable() {
const data = useSelector((state) => state.dish);
const [clicked1, setClicked1] = useState();
const [clicked2, setClicked2] = useState();
const [clicked3, setClicked3] = useState();
const dispatch = useDispatch();
function setAllStates(size) {
const newArray = new Array(size).fill(false);
setClicked1(newArray);
setClicked2(newArray);
setClicked3(newArray);
}
useEffect(() => {
if(data) setAllStates(data.length);
}, [data]);
}
Your code seems fine.
Possible bug may be -> data.length being 0.
To check add console.log(data.length) after data is set.
Now the code:
let data = useSelector((state) => state.dish);
console.log(data.length) // if this is zero , this is causing empty array
const [clicked1, setClicked1] = useState(new Array(data.length).fill(false));
...
Edit:
To ensure the latest value of data is used. We can use useEffect and add data as dependency which ensures that states are set once the data value is not null.
function Datatable() {
const data = useSelector((state) => state.dish);
const [clicked1, setClicked1] = useState();
const [clicked2, setClicked2] = useState();
const [clicked3, setClicked3] = useState();
const dispatch = useDispatch();
function setAllStates(size) {
const newArray = new Array(size).fill(false);
setClicked1(newArray);
setClicked2(newArray);
setClicked3(newArray);
}
useEffect(() => {
if(data) setAllStates(data.length);
}, [data]);
}
Code credits: #dr0nda
I have an array of data resembling the following:
data = [{name: 'A', data: 1}, {name: 'B', data: 2}]
I also have code resembling the following:
function ReportComponent({ data }) {
return data.map((datum) => (
<Typography>
{datum.name}: {datum.data}
</Typography>
));
}
which is called in
function ReportBox({ component }) {
const { data } = useFetchHook(component.urls)
// data returns exactly as expected, an array of objects
return (
<Box>
<Typography>
{component.title}
</Typography>
{data !== null && <ReportComponent data={data} />}
</Box>
);
}
My issue is, when I run the application, I only get one output from my data (when I console.log(data) it returns the data I showed above), either
A: 1 OR B:2. I expect there to be both present in the component. Any advice?
---- Update ----
useFetch function
import { useState, useEffect } from 'react';
function useFetch(urls) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
let i = urls.length - 1;
const result = [];
while (i >= 0) {
const abortCont = new AbortController();
console.log(`url ${i}`);
console.log(urls[i]);
fetch(urls[i], { signal: abortCont.signal }, { mode: 'cors' })
.then((res) => {
if (!res.ok) {
console.log('something went wrong with the data fetch');
}
return res.json(); // why?
})
.then((data) => {
result.push(data);
setData(result);
})
.catch((err) => {
if (err.name === 'AbortError') {
console.log('aborted');
} else {
setError(err.message);
}
});
i -= 1;
}
}, [urls]);
// console.log(data);
return { data, error };
}
export default useFetch;
--- Update DashBox ---
mport { Box, Grid, Container, Typography } from '#mui/material';
import ReportBox from './ReportBox';
function DashBox({ components }) {
// console.log(components);
return (
<Grid
item
columns={5}
sx={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-evenly',
alignItems: 'stretch',
marginTop: '20px',
marginLeft: '5px'
}}
>
{components.map((component) => (
<ReportBox component={component} />
))}
</Grid>
);
}
export default DashBox;
--- Update Page ---
export default function Page() {
const optionsFilter= [
'A',
'B',
'C'
];
const [filter, setFilter] = useState('A');
const componentsPage = [
{
title: 'One',
urls: [
`http://localhost:9000/page1?filter=${filter}`,
`http://localhost:9000/page2?filter=${filter}`
]
}
];
const componentsPageGraphs = [
{
title: 'OneGraph',
urls: [
`http://localhost:9000/page1?filter=${filter}`,
`http://localhost:9000/page2?filter=${filter}`
]
}
];
return (
<Page title="Page">
<Container>
<Typography variant="h4" sx={{ mb: 5 }}>
Page
</Typography>
<Container marginBottom="10px">
<Typography marginLeft="5px" variant="h5">
Filters
</Typography>
<Grid
columns={5}
sx={{
display: 'flex',
flexDirection: 'row',
alignItems: 'stretch',
marginTop: '10px',
marginLeft: '5px',
justifyContent: 'space-evenly'
}}
>
<Grid item sx={{ pr: 5 }}>
<DropDown
options={optionsFilter}
title="Filter Type"
setData={setFilter}
data={filter}
key="one"
/>
</Grid>
</Grid>
</Container>
<br />
<Box
container
sx={{ border: 2 }}
marginLeft="20px"
pr="20px"
pb="20px"
pl="20px"
width="100%"
>
<Typography variant="h3">Page Dashboard</Typography>
<DashBox components={componentsPage} />
</Box>
<Grid container spacing={2} marginTop="20px">
{componentsPageGraphs.map((component) => (
<Grid item xs={6}>
<Typography>{component.title}</Typography>
<LineChart xtype="category" urls={component.urls} />
</Grid>
))}
</Grid>
</Container>
</Page>
);
}
---- Update again with the suggested fetch, unfortunately still overwriting ---
import { useState, useEffect, useRef } from 'react';
const sameContents = (array1, array2) =>
array1.length === array2.length && array1.every((value, index) => value === array2[index]);
function useFetch(urls) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const urlsRef = useRef(null);
if (!urlsRef.current || !sameContents(urlsRef.current, urls)) {
urlsRef.current = urls.slice();
}
useEffect(() => {
const results = [];
if (!urlsRef.current) {
return;
}
const controller = new AbortController();
const { signal } = controller;
Promise.all(
urlsRef.current.map((url) => {
fetch(url, { signal, mode: 'cors' })
.then((res) => {
if (!res.ok) {
console.log('http issue');
}
return res.json();
})
.then((data) => {
if (!signal.aborted) {
results.push(data);
setData(results);
setError(null);
}
})
.catch((error) => {
if (signal.aborted) {
return;
}
setData(null);
setError(error);
});
return () => {
controller.abort();
};
})
);
}, [urlsRef.current]);
return { data, error };
}
export default useFetch;
Stack Snippet:
const {useState, useEffect} = React;
// Fake Typography component
const Typography = ({children}) => <div>{children}</div>;
// Fake Box component
const Box = ({children}) => <div>{children}</div>;
// Fake fetch hook
function useFetchHook(urls) {
const [data, setData] = useState(null);
useEffect(() => {
setTimeout(() => {
setData([
{name: "One", data: "Data for 'One'"},
{name: "Two", data: "Data for 'Two'"},
{name: "Three", data: "Data for 'Three'"},
]);
}, 500);
}, []);
return {data};
}
function ReportComponent({ data }) {
return data.map((datum) => (
<Typography>
{datum.name}: {datum.data}
</Typography>
));
}
function ReportBox({ component }) {
const { data } = useFetchHook(component.urls)
// data returns exactly as expected, an array of objects
return (
<Box>
<Typography>
{component.title}
</Typography>
{data !== null && <ReportComponent data={data} />}
</Box>
);
}
ReactDOM.render(<ReportBox component={{urls: [], title: "Example"}} />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
Your Page component creates a new componentsPage object with new urls arrays in the components every time it renders. Those new urls arrays are ultimately passed to useFetch (aka useFetchHook), where you have this structure:
function useFetch(urls) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
// ...code that fetches and sets `data`/`error`...
}, [urls]);
// console.log(data);
return { data, error };
}
That means that every time the urls parameter changes value (the old value isn't === the new value), it will repeat the fetch and update data or error.
There are various issues with the hook as well, the primary problem being that it does asynchronous work (a series of fetch calls) but doesn't check to be sure that the results its getting aren't outdated (because urls changed). More on that in a moment.
Since the urls arrays are recreated every time, useFetch does the fetches again every time, because no array is ever === any other array, even if they have the same contents:
console.log(["1", "2", "3"] === ["1", "2", "3"]); // false
So you need to:
Have useFetch only start a new series of fetches when the URLs really change. If it's given a new array with the same contents, it shouldn't do a new set of fetches.
useFetch should abort the fetches that are in progress if it's about to get a new set of urls, and shouldn't use the previous results if that's happened.
You seem to have started on #2 by using an AbortController, but nothing every called its abort method, so it didn't do anything.
Here's a version of useFetch that handles both of those things, see the comments:
const sameContents = (array1, array2) => {
return array1.length === array2.length &&
array1.every((value, index) => value === array2[index]);
};
function useFetch(urls) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const urlsRef = useRef(null); // A place to keep the URLs we're handling
if (!urlsRef.current || // Mounting, or
!sameContents(urlsRef.current, urls) // Called after mount with *different* URLs
) {
// Remember these URLs
urlsRef.current = urls.slice();
}
useEffect(() => {
if (!urlsRef.current) {
// Nothing to do
return;
}
// Use the same controller and signal for all the fetches
const controller = new AbortController();
const {signal} = controller;
// Use `Promise.all` to wait for all the fetches to complete (or one
// of them to fail) before setting `data`.
Promise.all(urlsRef.current.map(url =>
// Note: You had `{ mode: "cors" }` on its own as a third argument,
// but it should have been part of the second argument (`fetch`
// only takes two).
fetch(url, {signal, mode: "cors"})
.then(res => {
if (!res.ok) {
// HTTP error
throw new Error(`HTTP error ${res.status}`);
}
// HTTP okay, read the body of the response and parse it
return res.json();
})
))
.then(data => {
// Got all the data. If this set of results isn't out of date,
// set it and clear any previous error
if (!signal.aborted) {
setData(data);
setError(null);
}
})
.catch(error => {
// Do nothing if these results are out of date
if (signal.aborted) {
return;
}
// Clear data, set error
setData(null);
setError(error);
});
// Return a cleanup callback to abort the set of fetches when we get
// new URLs.
return () => {
controller.abort();
};
}, [urlsRef.current]); // <=== Use this instead of `urls`
return { data, error };
}
That's a sketch, I won't be surprised if you need to make small tweaks to it, but it should get you going the right way.
Following Situation.
I have a functional Parent Component like this:
function TestAutomationTab() {
const theme = createMuiTheme({
typography: {
htmlFontSize: 10,
useNextVariants: true,
},
});
const [szenarios, setSzenarios] = useState([]);
const [filterSzenario, setFilterSzenario] = useState('ALL');
const [data, setData] = useState([{}]);
const [runAll, setRunAll] = useState(false);
const [runAllButton, setRunAllButton] = useState('RUN ALL');
useEffect(() => {
fetchDistinctSzenarios();
fetchTestfaelle();
}, []);
async function fetchDistinctSzenarios() {
const response = await Api.getDistinctTestautoSzenarios();
setSzenarios(response.data);
setSzenarios(oldState => [...oldState, 'ALLE']);
}
function handleFilterChange(event) {
setFilterSzenario(event.target.value);
fetchTestfaelle();
}
async function fetchTestfaelle() {
const response = await Api.getAllOeTestfaelle();
response.data.forEach((e) => {
e.status = 'wait';
e.errorStatus = '';
e.statusText = '-';
});
setData(response.data);
}
function sendSingleCase(id) {
data.forEach((e) => {
if(e.id === id){
e.status = 'sending';
}
})
}
return (
<React.Fragment>
<MuiThemeProvider theme={theme}>
<div style={styles.gridContainer}>
<Upload />
<TestautomationSzenarioFilter
/>
<DocBridgePieChart />
<div style={styles.uebersicht}>
{filterSzenario.length ? <OeTestfallAccordion
choosenFilter={filterSzenario}
testData={data}
runAll={runAll}
sendSingleCase={sendSingleCase}
/> : <div>Wähle Szenario</div>}
</div>
</div>
</MuiThemeProvider>
</React.Fragment>
);
}
OeTestfallAccordion
function OeTestfallAccordion(props) {
const data = props.testData;
return (
<React.Fragment>
{data.map(e => (<OeTestfall
key={e.id}
szenario={e.szenario}
testid={e.testfallid}
json={e.json}
status={e.status}
runAll={props.runAll}
errorStatus={e.errorStatus}
statusText={e.statusText}
sendSingleCase={props.sendSingleCase}
/>))}
</React.Fragment>
);
}
OeTestfall
function OeTestfall(props) {
const { szenario, testid, json } = props;
const [open, setOpen] = useState(false);
function handleOpen(event) {
event.stopPropagation();
setOpen(true);
}
function handleClose() {
setOpen(false);
}
return (
<ExpansionPanel>
<ExpansionPanelSummary expandIcon={<ExpandMoreOutlined />}>
<OeTestfallSummary
szenario={szenario}
testid={testid}
json={json}
status={props.status}
handleClose={handleClose}
handleOpen={handleOpen}
open={open}
statusText={props.statusText}
errorStatus={props.errorStatus}
sendSingleCase={props.sendSingleCase}
/>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<div>ForNoError</div>
</ExpansionPanelDetails>
<ExpansionPanelActions>
<Button
variant="outlined"
color="primary"
>
Bearbeiten
</Button>
<Button
variant="outlined"
color="secondary"
>
Löschen
</Button>
</ExpansionPanelActions>
</ExpansionPanel>
);
}
OeTestfallSummery
function OeTestfallSummary(props) {
const { handleOpen } = props;
const [status, setStatus] = useState('');
const [statusText, setStatusText] = useState('');
const [errorStatus, setErrorStatus] = useState('');
useEffect(() => {
setErrorStatus(props.errorStatus);
setStatusText(props.statusText);
setStatus(props.status);
}, []);
return (
<div style={styles.summaryWrapper}>
<Typography align="center" variant="subtitle1">
TestID: {props.testid}
</Typography>
<Typography align="center" variant="subtitle1" style={{ fontWeight: 'bold' }}>
{props.szenario}
</Typography>
<Button
size="small"
variant="outlined"
color="primary"
onClick={handleOpen}
>
JSON
</Button>
<Tooltip title="VorneTooltip" style={styles.lightTooltip} placement="left">
<Chip
color="secondary"
variant="outlined"
label={status}
/>
</Tooltip>
<StatusChip
status={errorStatus}
/>
<OeJsonViewer json={JSON.parse(props.json)} open={props.open} handleClose={props.handleClose} stopEventPropagation />
<Tooltip
title="ToolTipTitel"
style={styles.lightTooltip}
placement="top"
>
<Chip
color="primary"
variant="outlined"
label={statusText}
/>
</Tooltip>
<Button variant="contained" color="primary" onClick={() => props.sendSingleCase(props.testid)} >
Run
</Button>
<Button variant="contained" color="primary" onClick={() => console.log(status)} >
test
</Button>
</div>
);
}
In my OeTestfallAccordion the prop testData does not update. If i try to console.log it inside my childComponent it has the old Value like before i execute the sendSinglecase function. What do i need to do, that i update the Data correctly that my child component gets notified that the props had changed and it has to rerender.
EDIT:
I tried some new things and can narrow down the problem. In my TestAutomationTab Component i send the whole data State to the OeTestfallAccordion Child Component. In this OeTestfallAccordion Component i split up the Array of Data which consists of multiple Objects like:
0: {id: 41, testfallid: 1, json: "{\"testCaseData\":{\"baseData\":{\"Check\":\"Thing…e\":\"alle\",\"tuwid\":\"2909\"}},\"testType\":\"Test\"}}", ID: null, businessId: null, …}
1: {id: 42, testfallid: 2, json: "{\"testCaseData\":{\"baseData\":{\"testfallid\":\"1…e\":\"alle\",\"tuwid\":\"2909\"}},\"testType\":\"Test\"}}", edcomAuftragsId: null, businessId: null, …}
When i hit the function sendSingleCase in my Parent Component TestAutomationTab i just change one single Parameter of the Object. The whole construct of Data keeps the same. The Child Component doesnt recognize that i changed something in the Object of Data.
But i dont know why? I also tried to useEffect on Props change in my Child COmponent when the props are changed. But it never gets executed even tho some attributes got updated inside the props.data.
function OeTestfallAccordion(props) {
const testData = props.testData;
const [data, setData] = useState(testData);
useEffect(() => {
setData(testData);
console.log("triggered");
}, [props]);
...
}
Okay things worked out a bit.
I changed the sendSingleCase function to first Copy the whole state in a Temp variable. Change one Attribute inside an Object and then setData (inside useState) with the tempData Variable. So the whole State gets renewed and the child components recognize the change and rerender.
But it seems not to be very fast. Always to copy the whole Data in a new Variable and then reassign it is very Ressource heavy. Is there a better solution?
function sendSingleCase(id) {
const tempState = [...data];
tempState.forEach((e) => {
if (e.testfallid === id) {
e.status = "pressed";
console.log(e.status);
}
});
setData(tempState);
}
I'm having an issue where useEffect isn't triggering a re-render based on useState changing or useState isn't changing which isn't triggering useEffect. I noticed this issue once I selected an asset that should update useState as the selected component and then I select another its no problem but once I select an asset that has already been selected.. nothing happens? Any suggestions or anything is greatly appreciated!! Thanks!
export default function SingleAsset({svg, name, size = '60', group}) {
const [assetType, setAssetType] = React.useState(null);
const [officeType, setOfficeType] = React.useState(null);
const [industrialType, setIndustrialType] = React.useState(null);
const [financingType, setFinancingType] = React.useState(null);
const [investmentType, setInvestmentType] = React.useState(null)
const acquistionStatus = useSelector(state => state.Acquisition)
const dispatch = useDispatch()
const classes = useStyles()
React.useEffect(() => {
if(financingType === 'Acquisition') {
const data = {financingType}
dispatch(updateFormData(data))
dispatch(toggleAcquisitionOn())
}
if(financingType) {
if(financingType !== 'Acquisition') dispatch(toggleAcquisitionOff())
const data = {financingType}
dispatch(updateFormData(data))
}
if(industrialType) {
const data = {industrialType}
dispatch(updateFormData(data))
}
if(officeType) {
const data = {officeType}
dispatch(updateFormData(data))
}
if(investmentType) {
const data = {investmentType}
dispatch(updateFormData(data))
console.log(data)
}
if(assetType) dispatch(updateAssetData(assetType))
console.log(financingType)
console.log(officeType)
console.log(industrialType)
console.log(investmentType)
},[investmentType,assetType,officeType,industrialType,financingType])
const handleSelect = (group, name) => {
switch(group) {
case 'main':
setAssetType(name)
break
case 'office':
setOfficeType(name)
break
case 'industrial':
setIndustrialType(name)
break
case 'financing':
setFinancingType(name)
break
case 'investment':
setInvestmentType(name)
break
default:
throw new Error('group not found')
}
}
return (
<Grid
className={classes.container}
item
>
<Grid
container
direction="column"
alignItems="center"
>
<IconButton onClick={() => handleSelect(group, name)}>
<img src={svg} color="white" height={size} />
</IconButton>
<Typography
variant="body1"
color="white"
align="center"
>
{name}
</Typography>
</Grid>
</Grid>
)
}
That's actually an expected behavior.
React uses "shallow comparison" (check this other great question for more on that), which essentially means it'll compare the previous and new value with ===. This is the reason one should not mutate state objects. Because of this, when your code tries to update the state to the same value it already has it won't actually do it... it's the same, so no re-render will be triggered.
To solve this, we can force React to update with some clever coding that will make React detect a state change:
// Setup a new state
const [, updateState] = useState();
// Create a function that will update state with a new object
// This works because {} === {} is always false, making React trigger a re-render
const forceUpdate = useCallback(() => updateState({}), []);
Your sample code would be something like this:
export default function SingleAsset({svg, name, size = '60', group}) {
const [, updateState] = useState();
const forceUpdate = useCallback(() => updateState({}), []);
const [assetType, setAssetType] = React.useState(null);
const [officeType, setOfficeType] = React.useState(null);
const [industrialType, setIndustrialType] = React.useState(null);
const [financingType, setFinancingType] = React.useState(null);
const [investmentType, setInvestmentType] = React.useState(null)
const acquistionStatus = useSelector(state => state.Acquisition)
const dispatch = useDispatch()
const classes = useStyles()
React.useEffect(() => {
if(financingType === 'Acquisition') {
const data = {financingType}
dispatch(updateFormData(data))
dispatch(toggleAcquisitionOn())
}
if(financingType) {
if(financingType !== 'Acquisition') dispatch(toggleAcquisitionOff())
const data = {financingType}
dispatch(updateFormData(data))
}
if(industrialType) {
const data = {industrialType}
dispatch(updateFormData(data))
}
if(officeType) {
const data = {officeType}
dispatch(updateFormData(data))
}
if(investmentType) {
const data = {investmentType}
dispatch(updateFormData(data))
console.log(data)
}
if(assetType) dispatch(updateAssetData(assetType))
console.log(financingType)
console.log(officeType)
console.log(industrialType)
console.log(investmentType)
},[investmentType,assetType,officeType,industrialType,financingType])
const handleSelect = (group, name) => {
switch(group) {
case 'main':
setAssetType(name)
break
case 'office':
setOfficeType(name)
break
case 'industrial':
setIndustrialType(name)
break
case 'financing':
setFinancingType(name)
break
case 'investment':
setInvestmentType(name)
break
default:
throw new Error('group not found')
}
}
return (
<Grid
className={classes.container}
item
>
<Grid
container
direction="column"
alignItems="center"
>
<IconButton onClick={() => {
handleSelect(group, name);
forceUpdate(); // <-- this is were the magic happens
}}>
<img src={svg} color="white" height={size} />
</IconButton>
<Typography
variant="body1"
color="white"
align="center"
>
{name}
</Typography>
</Grid>
</Grid>
)
}
Please note forcing a re-render should be a last resort, re-rendering can be a rather expensive operation and should be handled with care.