I'm trying to rewrite this code from class to a function. I've never used classes but I got this test code for a calendar app while learning react native but I seem to get stuck somewhere when I'm trying to replace componentDidUpdate to useEffect.
This is the old code:
export default class DaysInMonth extends React.PureComponent{
state = {
lastCalendarDayIndex: 0,
currentCalendarDayIndex: 0,
}
changeCurrentCalendarDayIndex = (index) => {
this.setState({
currentCalendarDayIndex: index
})
}
componentDidUpdate(prevProps, prevState){
if(this.state.currentCalendarDayIndex !== prevState.currentCalendarDayIndex){
this.setState({
lastCalendarDayIndex: prevState.currentCalendarDayIndex
})
}
}
render(){
return(
<>
{
this.props.row_days_array.map((rowData, index) => (
<CalendarRow
key = {'calendar row ' + index}
rowData = {rowData}
lastCalendarDayIndex = {this.state.lastCalendarDayIndex}
changeCurrentCalendarDayIndex = {this.changeCurrentCalendarDayIndex}
month_index = {this.props.month_index}
current_month_index = {this.props.current_month_index}
chooseDifferentMonth = {this.props.chooseDifferentMonth}
/>
))
}
</>
)
}
}
And this is the new code everything works except for some functions which has to do with the useEffect, I don't understand what properties I should add to get the same functionality as before. Thanks
export default function DaysInMonth({row_days_array, month_index, current_month_index, chooseDifferentMonth}) {
const [lastCalendarDayIndex, setLastCalendarDayIndex] = useState(0)
const [currentCalendarDayIndex, setCurrentCalendarDayIndex] = useState(0)
const changeCurrentCalendarDayIndex = (index) => {
setCurrentCalendarDayIndex(index)
}
useEffect(() => {
if(currentCalendarDayIndex !== currentCalendarDayIndex){
setLastCalendarDayIndex(currentCalendarDayIndex)
}
},[]);
return (
<>
{row_days_array.map((rowData, index) => (
<CalendarRow
key = {'calendar row ' + index}
rowData = {rowData}
lastCalendarDayIndex = {lastCalendarDayIndex}
changeCurrentCalendarDayIndex = {changeCurrentCalendarDayIndex}
month_index = {month_index}
current_month_index = {current_month_index}
chooseDifferentMonth = {chooseDifferentMonth}
/>
))}
</>
)
}
What this does:
useEffect(() => {
if(currentCalendarDayIndex !== currentCalendarDayIndex){
setLastCalendarDayIndex(currentCalendarDayIndex)
}
},[]);
It runs the code inside useEffect every time the array changes. Because it's an empty array it will just run once (once the component mounts, this is basically the old ComponentDidMount), to mimic the behaviour of ComponentDidUpdate you need to keep track of the props so it should be a matter of passing them into the array (so React can track when it changes):
useEffect(() => {
if(currentCalendarDayIndex !== currentCalendarDayIndex){
setLastCalendarDayIndex(currentCalendarDayIndex)
}
},[props]);
It's probably easier to change the destructuring you have in the component definition {row_days_array, month_index, current_month_index, chooseDifferentMonth} to props and then destructure a bit bellow, so you can use the whole object in you useEffect array
You can pass a callback in setState to access previous state, and pass currentCalendarDayIndex in useEffect dependency to update lastedCalendarState every currentCalendar changes. Hope this can help!
useEffect(() => {
setLastCalendarDayIndex((prevCalendarDayIndex) =>
prevCalendarDayIndex !== currentCalendarDayIndex
? currentCalendarDayIndex
: prevCalendarDayIndex)
}, [currentCalendarDayIndex])
Related
I have a component in which I have this useEffect:
const [charactersInfo, setCharactersInfo] = useState(null);
const [page, setPage] = useState(1);
useEffect(() => {
fetch(`https://api/api/character/?page=${page}`)
.then((res) => res.json())
.then((result) => {
setCharactersInfo(result);
});
}, [page]);
whenever my page state updates there is different data coming from the api as expected. but issue is whenever new setCharactersInfo(result) happens, it does not display the new data.
I am passing my setPage state function to this component as a prop:
<PaginationButtons
data={charactersInfo}
updatePage={(number) => {
setPage(number);
}}
/>
This is re-usable component which generates buttons and it works correctly everywhere except this specific component. any suggestions please?
import React, { useState, useEffect } from "react";
import "./PaginationButtons.css";
function PaginationButtons({ data, updatePage }) {
const [buttonsArr, setButtonsArr] = useState([]);
useEffect(() => {
const finalArray = [];
const { info } = data;
// Not the best solution for situations in which
// info.pages is big number(e.x 1000000) but since we know that
// it mostly will be 34 or less so we can just loop through it :)
for (let i = 1; i < info.pages + 1; i++) {
finalArray.push(
<button
className="page_button"
onClick={() => updatePage(i)}
key={Math.random()}
>
{i}
</button>
);
}
setButtonsArr(finalArray);
}, []);
return <div className="button_container">{buttonsArr.map((el) => el)}</div>;
}
export default PaginationButtons;
data prop is an object which contains various of stuff and on the them is the number of pages that should be displayed. in this specific case it 34 for so I use state and useEffect to loop through this number and store buttons in the state array and map it afterwards
You should handle data change in your child component as well.
pass data to useEffect dependency list.
useEffect(() => {
const finalArray = [];
const { info } = data;
// Not the best solution for situations in which
// info.pages is big number(e.x 1000000) but since we know that
// it mostly will be 34 or less so we can just loop through it :)
for (let i = 1; i < info.pages + 1; i++) {
finalArray.push(
<button
className="page_button"
onClick={() => updatePage(i)}
key={Math.random()}
>
{i}
</button>
);
}
setButtonsArr(finalArray);
}, [data]);
This should help you, no need to maintain state. and i see pages is not array its just key value pair.
function PaginationButtons({ data, updatePage }) {
const { info : { pages } } = data;
return (
<div className="button_container">
<button
className="page_button"
onClick={() => updatePage(pages || 0)}
key={pages}
>
{pages || 0}
</button>
</div>
);
}
The useEffect in PaginationButtons is using an empty dependency so it doesn't update when the data prop updates. From what I can tell you don't need the buttonsArr state anyway. It's also anti-pattern to store derived state from props. Just map the data array prop to the buttons.
Using random values is probably the least ideal method of specifying React keys. You can use the mapped array index, but you should use a unique property for each page element, or if there isn't one you should augment the data with a generated GUID.
function PaginationButtons({ data, updatePage }) {
return (
<div className="button_container">
{data.?info?.pages?.map((page, i) => (
<button
className="page_button"
onClick={() => updatePage(i)}
key={i} // <-- or page.id if available
>
{i}
</button>
))}
</div>
);
}
As stated in the other answers, you need to add data as a dependency. Also, you don't need to call map on buttonsArr, as you're not doing anything with its elements. Just use buttonsArr itself
So I'have a initial state in my wrapper component with two items
const initialData = {
first: clubData.applications[0].invoice_url,
second: clubData.applications[0].invoice_url_2,
};
const [invoiceFiles, setInvoiceFiles] = useState(initialData);
, the component has 2 childs and each child gets one of the items as file prop and also functions to change the state accroding to which property from state they use.
<AddInvoice
admin={admin}
clubData={clubData}
addFile={(file) => addFile("first", file)}
file={invoiceFiles.first}
deleteFile={() => deleteFile("first")}
invoiceUrl={clubData.applications[0].invoice_url}
/>
when i invoke the addFile function from AddInvoice component nothing happens in wrapper components, not even the useEffect function is called.Anyone know's why it is happening?
Here's my full wrapper code:
const AddInvoiceWrapper = ({ clubData, admin }) => {
const initialData = {
first: clubData.applications[0].invoice_url,
second: clubData.applications[0].invoice_url_2,
};
const [invoiceFiles, setInvoiceFiles] = useState(initialData);
const addFile = (index, file) => {
let newFiles = invoiceFiles;
newFiles[index] = file;
console.log(newFiles);
setInvoiceFiles(newFiles);
};
const deleteFile = (index) => {
let newFiles = invoiceFiles;
newFiles[index] = null;
setInvoiceFiles(newFiles);
};
useEffect(() => {
console.log("invoice files changed!");
}, [invoiceFiles]);
return (
<Row>
<Column>
<Paragraph>Dodaj fakturÄ™ (dev) </Paragraph>
<AddInvoice
admin={admin}
clubData={clubData}
addFile={(file) => addFile("first", file)}
file={invoiceFiles.first}
deleteFile={() => deleteFile("first")}
invoiceUrl={clubData.applications[0].invoice_url}
/>
</Column>
{invoiceFiles.first != null ? (
<Column>
<Paragraph>Dodaj koretkÄ™ faktury</Paragraph>
<AddInvoice
admin={admin}
clubData={clubData}
addFile={(file) => addFile("second", file)}
file={invoiceFiles.second}
deleteFile={() => deleteFile("second")}
invoiceUrl={clubData.applications[0].invoice_url_2}
/>
</Column>
) : null}
</Row>
);
};
export default AddInvoiceWrapper;
And this is also function in child components that triggers function in wrapper:
const handleChange = (e) => {
e.preventDefault();
addFile(e.target.files[0]);
};
The data is passed with no problems and in addFile function i get the proper new object but when i use "setInvoiceFiles" nothing happens, i see the change only when i make some changes in the code and the hot reload automaticily runs and refreshes the state.
Because the pointers are the same, React will not consider this a new data, and will not re-render. try setInvoiceFiles([...newFiles]); instead.
you can read about spread operator (...) here https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
When you create newFiles like this: let newFiles = invoiceFiles;
newFiles will have the same reference as invoiceFiles. When React compare object, it compares their references, since your newFiles and invoiceFiles have same reference (even though inner values are different), React won't mark it as an update so it won't trigger re-render or call useEffect
The solution is simple: Just change the line I mentioned to this:
let newFiles = {...invoiceFiles};
This time, newFiles will be a totally new variable with different reference.
You need to return new array to update state:
const addFile = (index, file) => {
setInvoiceFiles((preState) =>
preState.map((item, i) => {
return index === i ? file : item;
}),
);
};
I'm trying to create a re-usable component that wraps similar functionality where the only things that change are a Title string and the data that is used to populate a Kendo ComboBox.
So, I have a navigation menu that loads (at the moment) six different filters:
{
context && context.Filters && context.Filters.map((item) => getComponent(item))
}
GetComponent gets the ID of the filter, gets the definition of the filter from the context, and creates a drop down component passing in properties:
function getComponent(item) {
var filterDefinition = context.Filters.find(filter => filter.Id === item.Id);
switch (item.DisplayType) {
case 'ComboBox':
return <DropDownFilterable key={item.Id} Id={item.Id} Definition={filterDefinition} />
default:
return null;
}
}
The DropDownFilterable component calls a service to get the data for the combo box and then loads everything up:
const DropDownFilterable = (props) => {
const appService = Service();
filterDefinition = props.Definition;
console.log(filterDefinition.Name + " - " + filterDefinition.Id);
React.useEffect(() => {
console.log("useEffect: " + filterDefinition.Name + " - " + filterDefinition.Id);
appService.getFilterValues(filterDefinition.Id).then(response => {
filterData = response;
})
}, []);
return (
<div>
<div className="row" title={filterDefinition.DisplayName}>{filterDefinition.DisplayName}</div>
<ComboBox
id={"filterComboBox_" + filterDefinition.Id}
data={filterData}
//onOpen={console.log("test")}
style={{zIndex: 999999}}
dataItemKey={filterDefinition && filterDefinition.Definition && filterDefinition.Definition.DataHeaders[0]}
textField={filterDefinition && filterDefinition.Definition && filterDefinition.Definition.DataHeaders[1]}
/>
</div>
)
}
Service call:
function getFilterValues(id) {
switch(id) {
case "E903B2D2-55DE-4FA3-986A-8A038751C5CD":
return fetch(Settings.url_getCurrencies).then(toJson);
default:
return fetch(Settings.url_getRevenueScenarios).then(toJson);
}
};
What's happening is, the title (DisplayName) for each filter is correctly rendered onto the navigation menu, but the data for all six filters is the data for whichever filter is passed in last. I'm new to React and I'm not 100% comfortable with the hooks yet, so I'm probably doing something in the wrong order or not doing something in the right hook. I've created a slimmed-down version of the app:
https://codesandbox.io/s/spotlight-react-full-forked-r25ns
Any help would be appreciated. Thanks!
It's because you are using filterData incorrectly - you defined it outside of the DropDownFilterable component which means it will be shared. Instead, set the value in component state (I've shortened the code to include just my changes):
const DropDownFilterable = (props) => {
// ...
// store filterData in component state
const [filterData, setFilterData] = React.useState(null);
React.useEffect(() => {
// ...
appService.getFilterValues(filterDefinition.Id).then(response => {
// update filterData with response from appService
setFilterData(response);
})
}, []);
// only show a ComboBox if filterData is defined
return filterData && (
// ...
)
}
Alternatively you can use an empty array as the default state...
const [filterData, setFilterData] = React.useState([]);
...since the ComboBox component accepts an array for the data prop. That way you won't have to conditionally render.
Update
For filterDefinition you also need to make sure it is set properly:
const [filterDefinition, setFilterDefinition] = React.useState(props.Definition);
// ...
React.useEffect(() => {
setFilterDefinition(props.Definition);
}, [props.Definition]);
It may also be easier to keep filterDefinition out of state if you don't expect it to change:
const filterDefinition = props.Definition || {};
I have a component that creates several components using a loop, but I need to rerender only the instance being modified, not the rest. This is my approach:
function renderName(item) {
return (
<TextField value={item.value || ''} onChange={edit(item.id)} />
);
}
function renderAllNames(items) {
const renderedItems = [];
items.forEach(x => {
const item = React.useMemo(() => renderName(x), [x]);
renderedItems.push(item);
});
return renderedItems;
};
return (
<>
{'Items'}
{renderAllNames(names)};
</>
);
This yells me that there are more hooks calls than in the previous render. Tried this instead:
function renderAllNames(items) {
const renderedItems = [];
items.forEach(x => {
const item = React.memo(renderName(x), (prev, next) => (prev.x === next.x));
renderedItems.push(item);
});
return renderedItems;
};
Didn't work either... the basic approach works fine
function renderAllNames(items) {
const renderedItems = [];
items.forEach(x => {
renderedItems.push(renderName(x));
});
return renderedItems;
};
But it renders all the dynamic component everytime I edit any of the fields, so how can I get this memoized in order to rerender only the item being edited?
You're breaking the rules of hooks. Hooks should only be used in the top level of a component so that React can guarantee call order. Component memoisation should also really only be done using React.memo, and components should only be declared in the global scope, not inside other components.
We could turn renderName into its own component, RenderName:
function RenderName({item, edit}) {
return (
<TextField value={item.value || ''} onChange={() => edit(item.id)} />
);
}
And memoise it like this:
const MemoRenderName = React.memo(RenderName, (prev, next) => {
const idEqual = prev.item.id === next.item.id;
const valEqual = prev.item.value === next.item.value;
const editEqual = prev.edit === next.edit;
return idEqual && valEqual && editEqual;
});
React.memo performs strict comparison on all the props by default. Since item is an object and no two objects are strictly equal, the properties must be deeply compared. A side note: this is only going to work if edit is a referentially stable function. You haven't shown it but it would have to be wrapped in a memoisation hook of its own such as useCallback or lifted out of the render cycle entirely.
Now back in the parent component you can map names directly:
return (
<>
{'Items'}
{names.map(name => <MemoRenderName item={name} edit={edit}/>)}
</>
);
Every time I use react and the useEffect method my state variable renders twice. Once an empty variable and the next the desired variable. What can I try to help avoid this problem for now and in the future?
import React, { useState,useEffect } from "react";
export default function Member (props) {
const [team,setTeam] = useState([]);
useEffect(() => {
let array = ["hello","hi"];
setTeam(array);
}, [])
console.log(team);
return (
<>
{team.forEach(i => <p>{i}</p>)}
</>
)
}
You need to use map to render an array in JSX:
export default function Member(props) {
const [team, setTeam] = useState([]);
useEffect(() => {
let array = ["hello", "hi"];
setTeam(array);
}, []);
console.log(team);
return (
<>
{team.map(i => ( // use map here
<p>{i}</p>
))}
</>
);
}
forEach doesn't return anything, so you can't use it to render components like that.
Also in your code instead of using useEffect to setup initial state, you can just set it straight in useState:
export default function Member(props) {
const [team, setTeam] = useState(["hello", "hi"]);
console.log(team);
return (
<>
{team.map(i => ( // use map here
<p>{i}</p>
))}
</>
);
}
It is an abvious behavior.
Component render's first time with initial state. After useEffect (componentDidMount) again re-renders.
So you are getting two console.log.
To avoid this, you need to set the state initally,
const [team,setTeam] = useState(["hello","hi"]);
and remove useEffect.
Note: forEach won't print data, you need map here,
{team.map(i => <p key={i}>{i}</p>)} //provide a key here