How to pass function into React child? - reactjs

I have a parent Container, I plan to pass inside different child components that will accept callback.
Container:
const Container = ({children}) => {
const [selection, setSelection] = useState([]);
const setSelection = (returnObject) => {
setSelection(prev => [...selection, returnObject])
}
return(
<StyledContainer>
{children}
<Button>Search {selection}</Button>
</StyledContainer>
)
}
Container will have different children that all accept callback:
<Container><Child1 callback={}></Container>
<Container><Child2 callback={}></Container>
<Container><Child3 callback={}></Container>
Is there a way to pass component as a child to Container and for that Child to be using setSelection function as prop for Child's callback prop? (without Redux)

Yes you can override any props passed to the children as follows:
const Container = ({children}) => {
const [selection, setSelection] = useState([]);
const setSelection = (returnObject) => {
setSelection(prev => [...selection, returnObject])
}
return(
<StyledContainer>
{React.cloneElement(children, {callback: setSelection } }
<Button>Search {selection}</Button>
</StyledContainer>
)
}
Then you can use it as:
<Container><Child1/></Container>
<Container><Child2/></Container>
<Container><Child3/></Container>
And inside of each Child component, you can call callback() as need it.
React.cloneElement will create a copy from children passing additional props in this case only callback is passed as a prop you can pass as many as you need as a new object.
Details: https://reactjs.org/docs/react-api.html#cloneelement

Related

React (Native) how to make a component reusable when passing different data to iterate and callbacks from parent as well as child to grandchild?

I have a component thats opening and showing a modal that I want to reuse because almost everything I need in multiple places. Whats different is 1. data I am iterating through (property names are different) and 2. the button that triggers the modal has different styling. The problem is also that from the parent components I pass a callback, however, I also need to pass a callback to the part where I iterate/render data another callback coming from child component which is why I cannot just render the data iteration as children prop (thus always passing different data). I tried to implement a renderprop but also failed. I hope I explained not too confusing!! How do I do it?
const Parent1 = () => {
const [reportedLine, setReportedLine] = useState(null);
const [availableLines, setAvailableLines] = useState([]);
const [searchResultId, setSearchResultId] = useState('');
return (
<AvailableLinesSelector
data={availableLines}
disabled={searchResultId}
onSelect={setReportedLine}
/>
)
};
const Parent2 = () => {
const [line, setLine] = useState(null);
return (
<AvailableLinesSelector
data={otherData}
disabled={item}
onSelect={setLine}
/>
)
};
const AvailableLinesSelector = ({data, onSelect, disabled}) => {
const [isVisible, setIsVisible] = useState(false);
const [selectedLine, setSelectedLine] = useState('Pick the line');//placeholder should also be flexible
const handleCancel = () => setIsVisible(false);
const handleSelect = (input) => {
onSelect(input)
setSelectedLine(input)
setIsVisible(false);
};
return (
<View>
<Button
title={selectedLine}
//a lot of styling that will be different depending on which parent renders
disabled={disabled}
onPress={() => setIsVisible(true)}
/>
<BottomSheet isVisible={isVisible}>
<View>
{data && data.map(line => (
<AvailableLine //here the properties as name, _id etc will be different depending on which parent renders this component
key={line._id}
line={line.name}
onSelect={handleSelect}
/>
))}
</View>
<Button onPress={handleCancel}>Cancel</Button>
</BottomSheet>
</View>
)
};
You can clone the children and pass additional props like:
React.Children.map(props.children, (child) => {
if (!React.isValidElement(child)) return child;
return React.cloneElement(child, {...child.props, myCallback: callback});
});

Making the state of a component affect the rendering of a sibling when components are rendered iteratively

I have the following code:
export default function Parent() {
const children1 = someArrayWithSeveralElements.map(foo => <SomeView />);
const children2 = someArrayWithSeveralElements.map(foo => <SomeCheckbox />);
return (<>
{children1}
{/*Some other components*/}
{children2}
</>)
};
For a given element foo, there is a SomeView component that is conditionally rendered based on the state of a SomeCheckbox. I'm having trouble figuring out a way to have the state from the checkbox affect the rendering of the sibling view component.
Normally the solution would be to just declare the state hook in the parent component and pass them down to each child, but since the siblings are rendered via foreach loops it's impossible to do so.
My current solution is to also generate the state hooks for each foo in a loop as well, but that feels a bit hacky since it's better to avoid creating hooks inside of loops (it's worth nothing that someArrayWithSeveralElements is not intended to change after mounting).
Is there a more elegant alternative to solve this?
The solution is what you side, you need to create a state in the parent component and pass it to the children. and this will work for single component or bunch of them, the difference is just simple: use array or object as state.
const [checkboxesStatus, setCheckboxesStatus] = useState({// fill initial data});
const children1 = someArrayWithSeveralElements.map(foo =>
<SomeView
visibile={checkBoxesStatus[foo.id]}
/>);
const children2 = someArrayWithSeveralElements.map(foo =>
<SomeCheckbox
checked={checkBoxesStatus[foo.id]}
onChange={// set new value to foo.id key}
/>)
export default function Parent() {
const [states, setStates] = React.useState([]);
const children1 = someArrayWithSeveralElements.map((foo, i) => <SomeView state={states[i]} />);
const children2 = someArrayWithSeveralElements.map((foo, i) => {
const onStateChange = (state) => {
setStates(oldStates => {
const newStates = [...(oldStates || [])]
newStates[i] = state;
return newStates;
})
}
return <SomeCheckbox state={states[i]} onStateChange={onStateChange} />;
});
return (<>
{children1}
{/*Some other components*/}
{children2}
</>)
};
Use states in the parent componet.
Note: the element of states may be undefined.

React hook: Pass grid's object to parent component

I have a parent component which calls common grid component(child). The structure of the grid component is defined in the child component. The parent component just sends the data to the child component.
<Grid tableData={gridData} columnData={activeColumnDef} onselectedRows={isDisabledOnMultipleRowSelect}/>}
Child component:
const Grid = ({ tableData, columnData, ...props}) => {
const [gridApi, setGridApi] = useState(null);
const onGridReady = params => {
setGridApi(params.api);
setGridColumnApi(params.columnApi);
}
const refreshEntireGrid = () => {
gridApi.setRowData(gridrowData);
gridApi.refreshCells({force:true});
}
const getSelectedRowData = (event) => {
props.onselectedRows(event.api.getSelectedRows().length);
}
<div className="ag-theme-alpine-dark">
<AgGridReact
onGridReady={onGridReady}
defaultColDef={{
initialWidth: 270,
sortable: true,
filter: true,
resizable: true,
sizeColumnsToFit: true
}}
ref={gridRef}
applyColumnDefOrder={true}
headerCheckboxSelection={true}
suppressPaginationPanel={true}
columnDefs={gridcolumns}
rowData={gridrowData}
pagination={true}
paginationAutoPageSize={true}
rowSelection="multiple"
onSelect={(event) => getSelectedRowData(event)}
onSelectionChanged={getSelectedRowData}>
</AgGridReact>
</div>
In the parent component itself there are set of buttons. One of such buttons is RemoveButton which removes the selected rows from the grid and refreshButton that refreshes the grid. To capture the selected rows and the gridApi state, I have defined various functions.
I would like to achieve two things--
Either pass refreshEntireGrid function to the parent component so that when I click on the button in parent component this function is triggered.
Or find a way to pass the gridApi state to the parent object so that I can use it at multiple places. Instead of passing multiple methods from parent object as props, it would be more cleaner if I can pass gridApi to parent and use in whichever onClick method(in parent component) there is a need of it.
Is there a better solution instead of the two that I am thinking ? Please help ?
The solution is bit tricky. But quite easy if you understand below steps:
Create a callback in the parent component which takes in the data
needed as a parameter.
Pass this callback as a prop to the child
component.
Send data from the child component using the callback.
ParentComponent
function ParentComponent(props) {
let [gridValueFromChild, setGridValueFromChild] = useState(null);
let callback = valueFromChild => setGridValueFromChild(valueFromChild);
return (
<div>
<ChildComponent callbackFunc={callback} someValue={value} />
</div>
);
}
ChildComponent
function ChildComponent(props) {
const [gridApi, setGridApi] = useState(null);
const refreshEntireGrid = () => {
gridApi.setRowData(gridrowData);
gridApi.refreshCells({force:true});
// Send gridApi value to Parent
props.callbackFunc(gridApi)
}
// Incase you want to send data to parent at ComponentDidMount itself
useEffect(() => {
props.callbackFunc(gridApi)
}, [])
return (
<div>
...
</div>
);
}
If u want to pass gridApi value at the beginning itself or at any state change, You can use UseEffect hook and call callbackFunc there

How to test child components props method in the parent component test

I'm writing a test for my parent class which contains a child component. The code coverage reports that I haven't covered test for child components props method. Below is my structure of my components
export const CreateSRFullScreenPanel: React.FC<Props> = ({
interPluginContext,
srType,
errorMessage,
}: Props) => {
const [disableButton, setDisableButton] = React.useState(false);
const [submitSR, setSubmitSR] = React.useState(false);
const returnToHomePage = (): void => {
getRouteClient()
.changeRoute(getActiveRegionBaseUrl(state.regionName));
};
const cancelOp = interPluginContext.isValid()
? CancelAction
: returnToHomePage;
...
<childSR disableButton={disableButton} onSubmitComplete={() =>
setSubmitSR(false)}/>
<Button
type={ButtonType.Submit}
buttonStyle={ButtonStyle.Primary}
onClick={cancelOp}
>
Cancel
</Button>
};
When I wrote a test like below I was getting undefined for method calls.
it("check props method gets called", () => {
const wrapper = mount(
<ParentSR {...props} />
);
console.log(wrapper.find(CreateSR).props().onSubmitComplete()); // undefined
console.log(wrapper.find(CreateSR).props().disableButton()); // true
});
Also, when I click on a cancel button cancelOp method gets called. How do I mock returnToHomePage method calls?

Pass function via props cause useEffect infinite loop if I do not destructure props

I have a parent component with a state. And I want to pass a handler to set some state from a child component.
This is my parent component.
function ParentComponent() {
const [filters, setFilters] = useState({});
const setFiltersHandler = useCallback(filtersObj => {
setFilters(filtersObj);
}, []);
useEffect(() => {
// Do something and pass this to <Content /> component
}, [filters]);
return (
<div>
<Content filters={filters}>
<SideBarFilters applyFilters={setFiltersHandler} />
</div>
);
}
And this is my child component. This causes infinit loop.
const SideBarFilters = props => {
const [filterForm, setFilterForm] = useState({
specialities: {value: "all"}
});
// Some code with a input select and the handler to set filterForm
useEffect(() => {
let filterObj = {};
for (let key in orderForm) {
filterObj = updateObject(filterObj, {
[key]: orderForm[key]["value"]
});
}
props.applyFilters(filterObj);
}, [props, orderForm]);
return <OtherComponent />;
};
But if I destructure the props, it does not loop. Like this
const SideBarFilters = ({applyFilters}) => {
// same code as before
useEffect(() => {
// same as before
applyFilters(filterObj);
}, [applyFilters, orderForm]);
return <OtherComponent />;
};
My guess is that has something to do with how React compare props.
Maybe I should memo all props. But I think that is not a pattern
props object is referentially different each time parent re-renders(and re-renders SideBarFilters).
You should not fight that. Trying to find workaround you may run into brand new issues with stale date.
Destructure as you do, it's expected and suggested way to deal with dependencies in hooks.

Resources