SetState inside UseEffect causing infinite loop - reactjs

export const Upload = ({ initialfileList = [] }) => {
console.log("called...")
const [files,setFiles] = useState(initialfileList)
useEffect(() => {
setFiles(initialfileList)
}, [initialfileList])
return(
.....
)
}
I will not be sending initialfileList in intial render.
so I'm trying to update the state (files) when intialfileList value Changes.
But I'm not able to get why code is going into infinite loop.. Can someone help...
EDIT:
what I'm trying to achieve is there are two uploads in the form (say upload A and upload B) and checkbox as well. when the user uploads an image (say imgc) in Upload A and hits the checkbox, I wanted img c to be autouploaded in Upload B as well..

Ah, I see. How about this?
export const Upload = ({ initialfileList = [] }) => {
const [files, setFiles] = useState(initialfileList);
useEffect(() => {
function isDifferentFiles(originFiles, newFiles) {
return originFiles.length !== newFiles.length; // or whatever logic here.
}
if (isDifferentFiles(files, initialfileList)) {
setFiles(initialfileList);
}
}, [initialfileList]);
return <div>{files.length}</div>;
};
Btw, you might need to consider move the state to parent.

It sounds like you need to lift your state up - rather than storing fileLists locally, store a single fileList in the parent component, pass that down to both Upload A and Upload B, and also pass a setFileList function (which updates the state in the parent component).
i.e.:
// parent component
const Parent = () => {
const [fileList, setFileList] = useState([])
return (
<Upload fileList={fileList} setFileList={setFileList} />
<Upload fileList={fileList} setFileList={setFileList} />
)
}
const Upload = ({fileList, setFileList}) => {
return (
<UploadComponent files={fileList} onUpload={setFileList} />
)
}
This way, either upload component being updated will update both.

Related

Why do my ReactJS changes disappear on refreshing the page

I'm new to React and I'm trying to render a list of Pokemons.
I'm fetching the pokemon names from a local file and then using those names to trigger HTTP calls to my backend server, to get the pokemon images. Here's my code so far:
function PokemonList(props) {
const [pokemonList, setPokemonList] = useState([]);
const [isFetched, setIsFetched] = useState(false);
const [renderedList, setRenderedList] = useState([]);
useEffect(() => {
fetch(raw)
.then((r) => r.text())
.then((text) => {
setPokemonList(text.split("\n"));
setIsFetched(true);
});
}, []);
// I believe this is to blame, but I don't know why. On refresh, pokemonList becomes empty
useEffect(() => {
setRenderedList(populateRenderedList(pokemonList));
}, []);
return !isFetched ? null : (
<div className="list">
{renderedList}
<PaginationBar listSize={renderedList.length} list={renderedList} />
</div>
);
}
function populateRenderedList(pokemonList) {
let pokemonAPI = "https://pokeapi.co/api/v2/pokemon-form/";
const temp = [];
console.log(pokemonList);
pokemonList.forEach((pokemonName) => {
let renderedPokemon = (
<a className="pokemonLink" href={pokemonAPI + pokemonName.toLowerCase()}>
<PokemonDiv name={pokemonName.toLowerCase()} />
<h3>{pokemonName}</h3>
</a>
);
temp.push(renderedPokemon);
});
return temp;
}
As I have commented on the code, the 'pokemonList' renders fine when I make any changes to the PokemonList function. But the moment I refresh my page, 'pokemonList' becomes empty. Why is that?
I previously was not using 'useState' to populate my 'renderedList' list. So I believe the problem is happening because I'm using 'useState' , but I don't know why that's happening.
I had tried making 'renderedList' not a state, but I had to, for I am thinking about passing it as props to another child component, in order to change it's state.

Simulate user navigation with React Testing Library and React Router

I have a component that is meant to trigger when the user navigates away from a page. It wraps a formik form; if a user has unsaved changes, it attempts to save those changes as soon as the user attempts to navigate away. While the save is resolving, users will see a modal that says "saving..." If the save is successful, the user continues on to the next page. If it is unsuccessful, it displays a modal prompting them to either stay or move along. The component works fine, but I'm struggling to test it.
Component in question:
const AutoSaveUnsavedChangesGuard: React.FC<Props> = ({
when,
onLeave,
children,
ignoreRoutes = [],
submitForm,
}) => {
const { push } = useHistory();
const { error, savingStatus } = useSavingStatusContext();
const [nextLocation, setNextLocation] = React.useState<string>();
const [isShowing, setIsShowing] = React.useState<boolean>(false);
const [showUnsavedChangesModal, setShowUnsavedChangesModal] = React.useState<boolean>(false);
const [showSavingModal, setShowSavingModal] = React.useState<boolean>(false);
const handleBlockNavigation = (nextLocation: Location) => {
if (!!matchPath(nextLocation.pathname, ignoreRoutes)) {
return true;
}
setNextLocation(nextLocation.pathname);
setIsShowing(true);
submitForm();
return false;
};
React.useEffect(() => {
// Proceed to next location when there has been a navigation attempt and client no longer blocks it
if (!when && nextLocation) {
push(nextLocation);
}
}, [when, nextLocation, push]);
React.useEffect(() => {
// If we have an error and we have triggered the Prompt, display the unsaved changes guard.
setShowUnsavedChangesModal(!!error)
}, [error]);
React.useEffect(() => {
setShowSavingModal(savingStatus=== SavingStatusType.SAVING)
}, [savingStatus]);
return (
<React.Fragment>
<Prompt when={when} message={handleBlockNavigation}/>
<UnsavedChangesModal
show={showUnsavedChangesModal && isShowing}
onLeave={() => {
onLeave && onLeave();
}}
onStay={() => {
setNextLocation(undefined);
}}
onHide={() => {
setIsShowing(false);
setShowUnsavedChangesModal(false);
}}
/>
<SavingModal show={showSavingModal && isShowing} />
{children}
</React.Fragment>
);
};
export default AutoSaveUnsavedChangesGuard;
I'm trying to test behavior with react-testing-library. I'd like to simulate a user navigating away (IE call the message method in the rendered component with a new location), but I am struggling to do so. We had a function like the one below when we tested using enzyme.
const changeRouteLocation = (nextLocation: Location, wrapper: ShallowWrapper) => {
const prompt = wrapper.find(ReactRouter.Prompt);
const onNavigate = prompt.props().message as (location: Location) => string | boolean;
onNavigate(nextLocation);
};
Unfortunately, this component uses useEffect hooks that don't play nice with enzyme, and I must test it using react-testing-library. How can I simulate a user attempting to navigate to a new location with react-testing-library?
Edit: Adding what I have for testing code per a request. This code does not produce the desired outcome, and I honestly didn't expect it to.
const RenderingComponent = ({initialEntries})=>{
return(
<ThemeProvider>
<MemoryRouter initialEntries={initialEntries}>
<AutoSaveUnsavedChangesGuard {...defaults} />
</MemoryRouter>
</ThemeProvider>
)
}
beforeEach(() => {
jest.spyOn(ReactRouter, 'useHistory').mockReturnValue(makeHistory());
useSavingStatusContextSpy = jest.spyOn(useAutoSaveContextModule, 'useSavingStatusContext')
});
it('should render default. It should not show any modals when there are no errors and the route has not changed.', async () => {
// Default rendering. Works fine, because it's not meant to display anything.
const wrapper = render(
<RenderingComponent initialEntries={['/initial-value']} />
)
expect(screen.queryByText('Saving...')).toBeNull();
expect(screen.queryByText('Unsaved Changes')).toBeNull();
expect(wrapper).toMatchSnapshot()
});
it('should show the saving modal when the route location changes and saving status context is of type SAVING',()=>{
useSavingStatusContextSpy.mockReturnValueOnce(makeAutoSaveContext({savingStatus: SavingStatusType.SAVING}))
const {rerender, debug} = render(
<RenderingComponent initialEntries={["initial-value"]} />
)
rerender(<RenderingComponent initialEntries={['/mock-value','/mock-some-new-value']} />)
// I had hoped that re-rendering with new values for initial entries would trigger a navigation event for the prompt to block. It did not work.
debug()
const savingModal = screen.getByText('Saving...');
expect(savingModal).toBeVisible();
})
})

State is always one click late

I have a problem with my code, basically setRevalue is always one click late.
const Exchanger = () => {
const [revalue, setRevalue] = useState('null')
const [valueOfInput, setValueOfInput] = useState('');
const [valueOfSelect, setValueOfSelect] = useState('');
const handleClick = () => {
switch (valueOfSelect) {
case 'option_1':
axios.get('http://api.nbp.pl/api/exchangerates/rates/a/chf/?format=json')
.then((response) => {
setRevalue(response.data.rates[0].mid)
console.log(revalue);
})
break;
const handleInputChange = (event) => {
setValueOfInput(event.target.value);
}
const handleSelectChange = (event) => {
setValueOfSelect(event.target.value);
}
return(
<>
<Input
labelText= 'Amount'
onChange= { handleInputChange }
value = { valueOfInput }
/>
<Select selectValue={ valueOfSelect } onSelectChange={ handleSelectChange } />
<Button onClick={handleClick}>Klik!</Button>
</>
)
On the first click it returns a null which it was supposed to be before the click.
Any help would be much appreciated.
Hey so you have a clousure issue there
In JavaScript, closures are created every time a function is created, at function creation time.
At the time the .then() function is created, revalue has the value of the last render, That's why even after updating the state with setRevalue (and thus triggering a rerender), you get the "old value" (the one corresponding to the render in which the .then function was created).
I don't know exactly your use case, but seems like you can achieve whatever you are trying to achieve by doing:
const revalue = response.data.rates[0].mid
setRevalue(revalue)
console.log(revalue);
if you want to access to revalue right after setting it you have to use useEffect with revalue as a parameter , it would be like this
useEffect(()=>{
console.log(revalue);
},[revalue])
now every time revalue change you will have current result

Prevent context.consumer from re-rendering component

I have created the following context provider. In sort it's a toast generator. It can have multiple toasts visible at the same time.
It all worked great and such until I realized that the <Component/> further down the tree that called the const context = useContext(ToastContext) aka the consumer of this context and the creator of the toast notifications, was also re-rendering when the providerValue was changing.
I tried to prevent that, changing the useMemo to a useState hook for the providerValue, which did stop my re-rendering problem , but now I could only have 1 toast active at a time (because toasts was never updated inside the add function).
Is there a way to have both my scenarios?
export const withToastProvider = (Component) => {
const WithToastProvider = (props) => {
const [toasts, setToasts] = useState([])
const add = (toastSettings) => {
const id = generateUEID()
setToasts([...toasts, { id, toastSettings }])
}
const remove = (id) => setToasts(toasts.filter((t) => t.id !== id))
// const [providerValue] = useState({ add, remove })
const providerValue = React.useMemo(() => {
return { add, remove }
}, [toasts])
const renderToasts = toasts.map((t, index) => (
<ToastNote key={t.id} remove={() => remove(t.id)} {...t.toastSettings} />
))
return (
<ToastContext.Provider value={providerValue}>
<Component {...props} />
<ToastWrapper>{renderToasts}</ToastWrapper>
</ToastContext.Provider>
)
}
return WithToastProvider
}
Thank you #cbdeveloper, I figured it out.
The problem was not on my Context but on the caller. I needed to use a useMemo() there to have memoized the part of the component that didnt need to update.

Calling 'setState' of hook within context sequentially to store data resulting in race condition issues

I've created a context to store values of certain components for display elsewhere within the app.
I originally had a single display component which would use state when these source components were activated, but this resulted in slow render times as the component was re-rendered with the new state every time the selected component changed.
To resolve this I thought to create an individual component for each source component and render them with initial values and only re-render when the source components values change.
i.e. for the sake of an example
const Source = (props) => {
const { name, some_data} = props;
const [setDataSource] = useContext(DataContext);
useEffect(() => {
setDataSource(name, some_data)
}, [some_data]);
return (
...
);
}
const DataContextProvider = (props) => {
const [currentState, setState] = useState({});
const setDataSource = (name, data) => {
const state = {
...currentState,
[name]: {
...data
}
}
}
return (
...
)
}
// In application
<Source name="A" data={{
someKey: 0
}}/>
<Source name="B" data={{
someKey: 1
}}/>
The state of my provider will look like so;
{
"B": {
"someKey": 1
}
}
I believe this is because setState is asynchronous, but I can't think of any other solution to this problem
You can pass the function to setState callback:
setState((state) => ({...state, [name]: data}))
It takes the latest state in argument in any case, so it always safer to use if your update depends on previous state.

Resources