Reactjs closure when passing state to component - javascript

I got a react functional component:
const DataGrid = (props) =>
{
const [containerName, setContainerName] = useState("");
const [frameworkComponents, setFrameworkComponents] = useState(
{customLoadingOverlay: LoadingOverlayTemplate,
customNoRowsOverlay: UxDataGridCustomNoRows,
editButton: params => <ViewAndDeleteSetting {...params}
openAddConfigurationsWindow={openAddConfigurationsWindow}
onDeleteSetting={onDeleteSetting}/>,
});
useEffect(async () =>
{
if(props.containerName && props.containerName !== "")
{
setContainerName(props.containerName);
}
},[props.containerName]);
.
.
.
const onDeleteSetting = async (settingKey) =>
{
console.log("ON DELETE AND CONTAINER NAME:");
console.log(containerName); //HERE THE CONTAINER NAME IS EMPTY
...
}
return (
<UxDataGrid
frameworkComponents={frameworkComponents}/>
);
The container name inside useEffect exists and is not empty. As you can see in the comment in onDeleteSetting, the containerName is empty when this callback is invoked. I tried adding this to the useEffect after setContainerName:
setFrameworkComponents({customLoadingOverlay: LoadingOverlayTemplate,
customNoRowsOverlay: UxDataGridCustomNoRows,
editButton: params => <ViewAndDeleteSetting {...params}
openAddConfigurationsWindow={openAddConfigurationsWindow}
onDeleteSetting={onDeleteSetting}/>,
});
That didn't work.
How can I get the name inside the callback? There is no special need to leave that frameworkComponents struct in the state.. it can also be moved to somewhere else if you think its better

Try this in your useEffect, update the onDeleteSetting function with the new containerName when it's updated
.....
useEffect(async() => {
if (props.containerName && props.containerName !== "") {
setContainerName(props.containerName);
// move this function here
const onDeleteSetting = async(settingKey) => {
console.log("ON DELETE AND CONTAINER NAME:");
// use props.containerName since the state update is async
console.log(props.containerName);
...
}
// update your components with the updated functions
setFrameworkComponents(prevComponents => ({
...prevComponents,
editButton: params =>
<ViewAndDeleteSetting
{...params}
openAddConfigurationsWindow={openAddConfigurationsWindow}
onDeleteSetting={onDeleteSetting}
/>,
}));
}
}, [props.containerName]);
.....
This should provide the updated state with the updated function, if it works, I can add more details.

You almost certainly shouldn't be storing it in state. Props are essentially state controlled by the parent. Just use it from props. Copying props to state is usually not best practice.
If you're looking at one of the very rare situations where it makes sense to set derived state based on props, this page in the documentation tells you how to do that with hooks. Basically, you don't use useEffect, you do your state update right away.
Here's a full quote from the linked documentation:
How do I implement getDerivedStateFromProps?
While you probably don’t need it, in rare cases that you do (such as implementing a <Transition> component), you can update the state right during rendering. React will re-run the component with updated state immediately after exiting the first render so it wouldn’t be expensive.
Here, we store the previous value of the row prop in a state variable so that we can compare:
function ScrollView({row}) {
const [isScrollingDown, setIsScrollingDown] = useState(false);
const [prevRow, setPrevRow] = useState(null);
if (row !== prevRow) {
// Row changed since last render. Update isScrollingDown.
setIsScrollingDown(prevRow !== null && row > prevRow);
setPrevRow(row);
}
return `Scrolling down: ${isScrollingDown}`;
}
This might look strange at first, but an update during rendering is exactly what getDerivedStateFromProps has always been like conceptually.
If you did it the same way they did in that example, your component would still render with containerName set to the default state (""), it's just that it will then almost immediately re-render with the updated containerName. That makes sense for their example of a transition, but you could avoid that by making the prop's initial value the state's initial value, like this:
const DataGrid = (props) => {
const [containerName, setContainerName] = useState(props.containerName); // *** ONLY USES THE INITIAL PROP VALUE
const [frameworkComponents, setFrameworkComponents] = useState(
// ...
});
// *** Updates the state value (on the next render) if the prop changes
if (containerName !== props.containerName) {
setContainerName(props.containerName);
}
// ...
};
Every time the containerName prop changes, though, your component will render twice, which brings us back full circle to: Don't store it in state, just use it from props. :-)
Stepping back and looking at the component as a whole, I don't think you need any state information at all, but if your goal is to avoid having the frameworkComponents you pass UxDataGrid change unnecessarily, you probably want useMemo or React.memo rather than state.
For instance, with useMemo (but keep reading):
const DataGrid = ({containerName}) => {
const frameworkComponents = useMemo(() => {
const onDeleteSetting = async (settingKey) => {
console.log("ON DELETE AND CONTAINER NAME:");
console.log(containerName);
// ...
};
return {
customLoadingOverlay: LoadingOverlayTemplate,
editButton: params => <ViewAndDeleteSetting {...params}
openAddConfigurationsWindow={openAddConfigurationsWindow}
onDeleteSetting={onDeleteSetting} />,
};
}, [containerName]);
return (
<UxDataGrid frameworkComponents={frameworkComponents} />
);
};
But if componentName is your only prop, it may well be even simpler with React.memo:
const DataGrid = React.memo(({containerName}) => {
const onDeleteSetting = async (settingKey) => {
console.log("ON DELETE AND CONTAINER NAME:");
console.log(containerName);
// ...
};
return (
<UxDataGrid frameworkComponents={{
customLoadingOverlay: LoadingOverlayTemplate,
editButton: params => <ViewAndDeleteSetting {...params}
openAddConfigurationsWindow={openAddConfigurationsWindow}
onDeleteSetting={onDeleteSetting} />,
}} />
);
});
React.memo memoizes your component, so that your component function is only ever called again when the props change. Since everything in the component needs to update based on the componentName prop changing, that looks like a good match (but I don't know what UxDataGrid is).

The problem was with how I tried passing props to ViewAndDeleteSetting. If you want to pass prop to a cell rendered component, you shouldn't be doing it in frameworkComponents, but rather you need to do it in the column definition like this:
useEffect(() =>
{
let columns = [{headerName: '', cellRenderer: 'editButton', width: 90, editable: false,
cellRendererParams: {
openAddConfigurationsWindow: openAddConfigurationsWindow,
onDeleteSetting: onDeleteSetting
}},
.. other columns
]
setColumnDefinition(columns);
},[props.containerName]);
The columns with the cellRendererParams do gets recreated in the useEffect when the name changes, and then the component can access this params regularly via its props

Related

react testing library ternary operator, finding right component

I'm beginner with React testing, learning by coding, here i have a component 'cam.tsx'
i want to test it, when i want to test Add function it goes straight like this, but when i want to test Update function it still shows Add function in my test, how to test both of them ?
Add and Update functions are forms where user can fill.
describe("Testing component ", () => {
const Camera = (): RenderResult =>
render(
<Provider store={store}>
<Cam
}}
/>{" "}
</Provider>
);
test("Cam", () => {
Camera();
const name = screen.queryByTestId(/^AddName/i);
});
});
cam.tsx:
const ADD = "ADD";
let [state, setState] = useState<State>({mode: ADD });
if (props.mode) {
state.mode = props.mode;
}
const option = state.mode;
return (
<React.Fragment>
<div data-testid="header">
{option == ADD ? Add() : <></>}
{option == UPDATE ? Update() : <></>}
</div>
</React.Fragment>
Basically cam.tsx is a component which has two forms one for updating camera and another for adding new camera.When user clicks add/update icon then cam component gets 'mode' via props ' state.mode = props.mode '
English is not my mother language, so could be mistakes
Here is how to test a component that conditionally renders components from state and can be updated via props.
import {render, screen} from '#testing-library/react';
import {Cam} from './Cam';
test('renders add by default', () => {
render(<Cam/>);
expect(screen.getByTestId('addForm'))
.toBeInTheDocument();
expect(screen.queryByTestId('updateForm'))
.not.toBeInTheDocument();
});
test('renders edit by passing props', () => {
const {rerender} = render(<Cam mode={undefined}/>);
rerender(<Cam mode={'UPDATE'} />)
expect(screen.getByTestId('updateForm'))
.toBeInTheDocument();
expect(screen.queryByTestId('addForm'))
.not.toBeInTheDocument();
});
However, it is known in the React community that updating state via props is usually an anti-pattern. This is because you now have two sources of truth for state and can be easy to have these two states conflicting. You should instead just use props to manage rendering.
If state comes from a parent component, use props.
export function Cam(props) {
const option = props.mode;
return (
<div data-testid="header">
{option === ADD ? Add() : <></>}
{option === UPDATE ? Update() : <></>}
</div>
);
}
If you really want to keep state in the child component even if props are passed in, you should update props in an useEffect hook. Additionally, you should use the setState function rather than setting state manually state.mode = props.mode
Use the useEffect hook to update state via props.
...
const [state, setState] = useState({mode: ADD});
useEffect(() => {
if (props.mode) {
setState({mode: props.mode});
}
}, [props.mode]) <-- checks this value to prevent infinite loop.
const option = state.mode;
return (
...

How to trigger re-render in Parent Component from Child Component

, Using props I was able to effectively pass state upwards from my child component to its parent, but a change in the state does not cause a re-render of the page.
import React, { useState } from "react";
export default function App() {
const AddToList = (item) => {
setText([...text, item]);
};
const removeFromList = (item) => {
const index = text.indexOf(item);
setText(text.splice(index, 1));
};
const [text, setText] = React.useState(["default", "default1", "default2"]);
return (
<div className="App">
<div>
<button
onClick={() => {
AddToList("hello");
}}
>
Add
</button>
</div>
{text.map((item) => {
return <ChildComponent text={item} removeText={removeFromList} />;
})}
</div>
);
}
const ChildComponent = ({ text, removeText }) => {
return (
<div>
<p>{text}</p>
<button
onClick={() => {
removeText(text);
}}
>
Remove
</button>
</div>
);
};
In the snippet, each time AddToList is called, a new child component is created and the page is re-rendered reflecting that. However, when i call removeFromList on the child component, nothing happens. The page stays the same, even though I thought this would reduce the number of childComponents present on the page. This is the problem I'm facing.
Updated Answer (Following Edits To Original Question)
In light of your edits, the problem is that you are mutating and passing the original array in state back into itself-- React sees that it is receiving a reference to the same object, and does not update. Instead, spread text into a new duplicate array, splice the duplicate array, and pass that into setText:
const removeFromList = (item) => {
const index = text.indexOf(item);
const dupeArray = [...text];
dupeArray.splice(index, 1);
setText(dupeArray);
};
You can see this working in this fiddle
Original Answer
The reason React has things like state hooks is that you leverage them in order to plug into and trigger the React lifecycle. Your problem does not actually have anything to do with a child attempting to update state at a parent. It is that while your AddToList function is properly leveraging React state management:
const AddToList = (item) => {
setText([...text, item]);
};
Your removeFromList function does not use any state hooks:
const removeFromList = (item) => {
const index = text.indexOf(item);
text.splice(index, 1); // you never actually setText...
};
...so React has no idea that state has updated. You should rewrite it as something like:
const removeFromList = (item) => {
const index = text.indexOf(item);
const newList = text.splice(index, 1);
setText(newList);
};
(Also, for what it is worth, you are being a little loose with styles-- your AddToList is all caps using PascalCase while removeFromCase is using camelCase. Typically in JS we reserve PascalCase for classes, and in React we also might leverage it for components and services; we generally would not use it for a method or a variable.)

React TypeError: Cannot use 'in' operator to search for 'length' in null

I want to access a local array file, filter it, and store the filtred array as a state, finally pass the state to child component.
// array file
const originalData =
[
{ "ComName":"A", "SDGs":"1", "No":1 },
{ "ComName":"B", "SDGs":"2", "No":2 },
...
]
I use getComany() and filterCompany() to get the filtred data, and store it in filtredCompany as a state. But it turns out TypeError: Cannot use 'in' operator to search for 'length' in null.
// Services.js
export function getCompany() {
const companyList = originalData;
return companyList;
}
export function filterCompany(comType) {
// filter detail
let companyList = getCompany();
let filtredMatchCompany = getCompany().filter(type => type.SDGs === comType && type.No == comType);
let matchCom = _.map(filtredMatchCompany, 'ComName');
let filtredCompany = companyList.filter((type)=> matchCom.includes(type.ComName))
// Filter.js
export default function Filter() {
const [filtredCompany,setFiltredCompany] = useState(null);
useEffect(() => {
setFiltredCompany(getCompany());
}, []);
function handleD3(e) {
let typeCompany = e.target.value;
typeCompany !== "all"
? setFiltredCompany(filterCompany(typeCompany))
: setFiltredCompany(getCompany());
}
const outputFilter = filtredCompany;
return (
<>
{buttons &&
buttons.map((type, series) => (
<>
<button key={series} value={type.no} onClick={handleD3}>
{type.name}
</button>
</>
))}
<>
<ObjectD3 outputFilter = {filtredCompany}/>
</>
</>
}
The error might come from initial state null. I try to fix that, and change the initial state to
const [filtredCompany,setFiltredCompany] = useState([]) . TypeError doesn't occur, but setState, which is setFiltredCompany , doesn't work anymore.
const [filtredCompany,setFiltredCompany] = useState(null);
console.log(filtredCompany)
// *click button*
// the filtred data I want
const [filtredCompany,setFiltredCompany] = useState([]);
console.log(filtredCompany)
// *click button*
// []
Does anyone know how to handle this situation or better idea to pass data? Thank you so much in advanced!
Source code here
Let go through a part of your code here.
First of all, for a React Component, the lifecycle methods are executed in the following order: constructor -> render -> componentDidMount.
Within the Filter component, you are setting initial state like this:
useEffect(() => {
setFiltredCompany(getCompany());
}, []);
Now, one thing to remember is all the setState() functions and the useEffect hook, are asynchronous, that is, they complete their execution at some time in the future. So, when React renders your app, by the time ObjectD3 component is rendered, the useEffect hook has not executed, so
the ObjectD3 receives null as a prop and the statement in ObjectD3
this.dataset = this.props.outputFilter;
assigns null to the dataset, thereby giving you the error.
A better way to do it, is to implement another lifecycle method in ObjectD3, named componentDidUpdate, where you can compare the changes in props, since the update and take necessary actions.
Check the updated version of code here.

Too many re-render problems with React

I have some cards in my application that can lead to another pages through clicks. So I have a main component that contains a button like this:
function MainComponent(props) {
.
.
.
const handleClick = (key) => {
history.push("/exampleurl/" + key);
};
Then according to the key passed, I have to make a request that gives me some information required to display it. As default I have my initial state as null, and when it completes the request, it changes to the object I got. But as soon as I click on the card, I get the re-render error.
function MyComponent(props) {
let { key } = useParams();
const [myObject, setMyObject] = React.useState(null)
useEffect(() => {
axios.get('/myendpoint/' + key).then( response => {
let myObject = response.data
setMyObject(myObject)
})
}, [key])
I suppose that the solution is avoiding the key value to update when it changes the state. But i am not finding the solution to this trouble.
Edit: The route that leads to the components:
<Route path="/inbox">
<MainComponent />
</Route>
<Route path="/exampleurl/:key">
<NewComponent />
</Route>
I think the problem is related to the handleClick function.
Every time this method is called, you push a new entry to the history stack. Which analyze your defined routes and render the linked component. In your case, it is the same component, but I am not sure if the router is capable to determine it, therefore I would expect a re-render.
Maybe a solution would be to include another state which is responsible to inform the component of the current obj being displayed on the screen. So key will be responsible only for the route parameter and this new state will be responsible for the internal navigation.
function MyComponent(props) {
let { key } = useParams();
const [myObject, setMyObject] = React.useState(null)
const [displayedObj, setDisplayedObj] = React.useState('');
useEffect(() => {
axios.get('/myendpoint/' + key).then( response => {
let myObject = response.data
setMyObject(myObject)
setDisplayedObj(key)
})
}, [key, displayedObj]) // we listen for displayedObj too
and then in the handleClick we update this new state. This will trigger the useEffect and therefore update the myObject state to the new value:
const handleClick = (key) => {
setDisplayedObj(key);
// This will trigger the useEffect and refresh
// the data displayed without reloading the page
};

React: how can I force state to update in a functional component?

This function component has a template method that calls onChangeHandler, which accepts a select value and updates state. The problem is, state does not update until after the render method is called a second time, which means the value of selected option is one step ahead of the state value of selectedRouteName.
I know there are lifecycle methods in class components that I could use to force a state update, but I would like to keep this a function component, if possible.
As noted in the code, the logged state of selectedRouteDirection is one value behind the selected option. How can I force the state to update to the correct value in a functional component?
This question is not the same as similarly named question because my question asks about the actual implementation in my use case, not whether it is possible.
import React, { useState, Fragment, useEffect } from 'react';
const parser = require('xml-js');
const RouteSelect = props => {
const { routes } = props;
const [selectedRouteName, setRouteName] = useState('');
const [selectedRouteDirection, setRouteDirection] = useState('');
//console.log(routes);
const onChangeHandler = event => {
setRouteName({ name: event.target.value });
if(selectedRouteName.name) {
getRouteDirection();
}
}
/*
useEffect(() => {
if(selectedRouteName) {
getRouteDirection();
}
}); */
const getRouteDirection = () => {
const filteredRoute = routes.filter(route => route.Description._text === selectedRouteName.name);
const num = filteredRoute[0].Route._text;
let directions = [];
fetch(`https://svc.metrotransit.org/NexTrip/Directions/${num}`)
.then(response => {
return response.text();
}).then(response => {
return JSON.parse(parser.xml2json(response, {compact: true, spaces: 4}));
}).then(response => {
directions = response.ArrayOfTextValuePair.TextValuePair;
// console.log(directions);
setRouteDirection(directions);
})
.catch(error => {
console.log(error);
});
console.log(selectedRouteDirection); // This logged state is one value behind the selected option
}
const routeOptions = routes.map(route => <option key={route.Route._text}>{route.Description._text}</option>);
return (
<Fragment>
<select onChange={onChangeHandler}>
{routeOptions}
</select>
</Fragment>
);
};
export default RouteSelect;
Well, actually.. even though I still think effects are the right way to go.. your console.log is in the wrong place.. fetch is asynchronous and your console.log is right after the fetch instruction.
As #Bernardo states.. setState is also asynchronous
so at the time when your calling getRouteDirection();, selectedRouteName might still have the previous state.
So to make getRouteDirection(); trigger after the state was set.
You can use the effect and pass selectedRouteName as second parameter (Which is actually an optimization, so the effect only triggers if selectedRouteName has changed)
So this should do the trick:
useEffect(() => {
getRouteDirection();
}, [selectedRouteName]);
But tbh.. if you can provide a Stackblitz or similar, where you can reproduce the problem. We can definitely help you better.
setState is asynchronous! Many times React will look like it changes the state of your component in a synchronous way, but is not that way.

Categories