Parent update causes remount of context consumer? - javascript

I have a wrapper component that creates a context consumer and passes the context value as a prop to a handler component. When the parent of the wrapper component updates, it's causing my handler component to remount instead of just update.
const Wrapper = forwardRef((props, ref) => {
class ContextHandler extends Component {
componentDidMount() {
// handle the context as a side effect
}
render() {
const { data, children } = this.props;
return (
<div ref={ref} {...data}>{children}</div>
);
}
}
return (
<Context.Consumer>
{
context => (
<ContextHandler
data={props}
context={context}
>
{props.children}
</ContextHandler>
)
}
</Context.Consumer>
);
});
I put the wrapper inside a parent component:
class Parent extends Component {
state = {
toggle: false
}
updateMe = () => {
this.setState(({toggle}) => ({toggle: !toggle}))
}
render() {
const { children, data } = this.props;
return (
<Wrapper
onClick={this.updateMe}
{...data}
ref={me => this.node = me}
>
{children}
</Wrapper>
)
}
}
When I click on the Wrapper and cause an update in Parent, the ContextHandler component remounts, which causes its state to reset. It should just update/reconcile and maintain state.
What am I doing wrong here?

Your ContextHandler class is implemented within the render function of the Wrapper component which means that an entirely new instance will be created on each render. To fix your issue, pull the implementation of ContextHandler out of the render function for Wrapper.

Related

Can we pass props from Parent to Child component in React, and set the props as state of child component?

const Parent=()=>{
return(
)
}
const Child=({data})=>{
return (
{data}
)
}
Can I set the props in Child component to its state?
Yes you can. Something like:
class Child extends Component {
constructor(props){
const {myProp} = props;
this.state = {myNumber: myProp}
render(){
const {myNumber} = this.state;
return <div>{myNumber}</div>
}
class Parent extends Component {
render () {
return <Child myProp={5} />
}
}
BUT: if your "parent" component refreshes, so does the child and the state is reverted back to the value set by the parent, and all the state changes you did on the child are lost.
Yeah here made a code sandbox example showing how to do it: https://codesandbox.io/s/stack-pass-props-fgq67n
Child component looks like this
const Input = ({ string }) => {
const [value, setValue] = useState(string);
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
};
Parent Component:
export default function App() {
return (
<div className="App">
<Input string="default" />
</div>
);
}
Sometimes if the parent reloads the passed props may reset to defaults
There is a concept in React known as controlled and uncontrolled components, When your component only have props and no state inside itself then it a controlled component, that is controlled by the parent component.
Also, it is not advisable to convert the props to the state of the child component, if you want to change the value of the props, just pass an additional method as a props which will be called by the Child to update the value of the props, let show you with an example
const Parent = () => {
const [number, setNumber] = useState(0);
const updateNumberHandler = (numberToUpdate) => setNumber(numberToUpdate)
return <Child number={number} onUpdateNumber={updateNumberHandler} />
}
const Child = (props) => {
const { number, onUpdateNumber } = props;
return <button onClick={() => onUpdateNumber(number + 1)}>Updated Number: {number}</button>
}
Here you can see I am passing two props one value and one method to update that value, when the onUpdateNumber method is called the value on the parent is updated so it gets re-render and also the child gets re-render with the updated number value.

Iterating over child components and rendering each wrapped in a higher order component

I am currently experiencing an issue when it comes to wrapping the child elements of a parent element in a higher order component and then rendering them.
Consider the following structure:
return (
<div className="App">
<FocusProvider>
<TestComponent testProp={'Foo'}/>
<TestComponent testProp={'Foo'}/>
<TestComponent testProp={'Foo'}/>
</FocusProvider>
</div>
);
Where FocusProvider is the parent element and TestComponent is the child element that needs to be wrapped in a higher order component that provides lifecycle methods to it as well as inject props.
And then the higher order component called hoc which overrides the prop for TestComponent and provides a lifecycle method to it as well looks like:
const hoc = (WrappedComponent, prop) => {
return class extends React.Component {
shouldComponentUpdate = (prevProps, prop) => {
return !prevProps === prop
}
render(){
return <WrappedComponent testProp={prop}/>
}
}
}
The render method of FocusProvider looks like :
render(){
return(
this.props.children.map(child => {
let Elem = hoc(child, 'bar')
return <Elem/>
})
)
}
When I try and render that I get Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
When I try and change it to :
render(){
return(
this.props.children.map(child => {
let elem = hoc(child, 'bar')
return elem
})
)
}
Nothing is returned from render. I am confused because I can render the chil components directly, but not the child components wrapped in the HOC:
render(){
return(
this.props.children.map(child => {
return child //This works
})
)
}
I want to avoid using React.cloneElement as I don't want to trigger re-renders by cloning the child elements every time the parent updates.
Any help would be appreciated
hoc is a function which returns a Component not jsx. You cannot wrap the children in a HOC like that.
But you can wrap just FocusProvider and pass the prop down to it's children using cloneElement. There is no problem in use cloneElement like this. Is a common pattern actually.
function App() {
return (
<div className="App">
<FocusProvider bar="baz">
<Child />
<Child />
<Child />
</FocusProvider>
</div>
);
}
const withHOC = Component => props => {
return <Component foo="bar" {...props} />;
};
const FocusProvider = withHOC(({ children, foo, bar }) => {
return React.Children.map(children, child => {
return React.cloneElement(child, { foo, bar });
});
});
const Child = ({ foo, bar }) => (
<>
{foo}
{bar}
</>
);

Is React context provider not mounted before child components?

How do I make sure I set a value in the context provider before components are mounted?
In the code example below, the console.log in the child component(Dashboard) will be logged first (as undefined). Why is that and is there any way for me to make sure the value is set before that component is mounted?
App.js
render() {
return (
<div className='App'>
<ContextProvider>
<Dashboard />
</ContextProvider>
</div>
);
}
ContextProvider.js
componentDidMount = () => {
this.setState({value: 1})
console.log(this.state.value);
}
Dashboard.js
componentDidMount = () => {
console.log(this.context.value);
}
Children are rendered first. Regardless of that, setState is asynchronous, so a context will be provided to consumers asynchronously.
In case there's a necessity for children to wait for a context, they should be either conditionally rendered:
render() {
this.context.value && ...
}
Or be wrapped with context consumer which can be written as a HOC for reuse:
const withFoo = Comp => props => (
<FooContext.Consumer>{foo => foo.value && <Comp {...props}/>}</FooContext.Consumer>
);

React overwrite component function

I'm trying to do the following, currently I have 3 components:
Parent.js:
class Parent extends Component {
applyFilters = () => {console.log("applying original filters")}
setApplyFilters = (callback) => {
this.applyFilters = () => callback();
}
render(){
return(
<div>
<Filters applyFilters={this.applyFilters} />
<Screen1 setApplyFilters={this.setApplyFilters} />
</div>
)
}
}
Filters.js:
class Filters extends Component {
onButtonPress = () => {
this.props.applyFilters(),
}
render(){
...
}
}
Screen1.js:
class Screen1 extends Component {
applyFilter = () => {
console.log("Applying filters using the callback function in Screen1");
}
componentDidMount = () => {
this.props.setApplyFilters(() => this.applyFilters());
}
render(){
...
}
}
I have a Filters component that is common to all the screens. I have multiple screens of the type Screen1.
I want to on componentDidMount of the current screen pass the applyFilter function as a callback to the parent, and from the parent pass the applyFilter as a prop to the Filter component. When the onButtonPressed handler of the filter is called, it should execute the applyFilter callback for the mounted screen.
But for some reason it just prints to the console "applying original filters" which is the original string, as if the function is not being overwritten with the callback from the screen.
How do I do this correctly?
The this.applyFilters is resolved early during the render process to () => {console.log("applying original filters")}
<Filters applyFilters={this.applyFilters} />
In this scenario you only care for its value at the time that the function it references is invoked, so you want to set it lazily.
<Filters applyFilters={() => this.applyFilters()} />
This way when applyFilters invoked in Filters component, the value that is referenced in this.applyFilters is resolved and then invoked.
After changing applyFilters on the parent, rerender it i.e. with forceUpdate to make Filter component receive the changed function.
class Parent extends Component {
applyFilters = () => {console.log("applying original filters")}
setApplyFilters = (callback) => {
this.applyFilters = () => callback();
this.forceUpdate();
}
render(){
return(
<div>
<Filters applyFilters={this.applyFilters} />
<Screen1 setApplyFilters={this.setApplyFilters} />
</div>
)
}
}

React - Array of children components not rerendering with props expression set in a callback

I understand that the wording of the question is slightly nebulous, so I will expand. This is a personal project of mine that I have taken up to learn some React basics and familiarize myself with socket.io
I have a CollapsibleList component, and a NestedList component, which renders an array of the CollapsibleList components.
NestedList has an event handler that gets set in componentWillMount of the component. The event is when a menu arrives via socket.io from my server. When the menu arrives, a new CollapsibleList is added to the array, and state is changed to trigger a rerender. The events are triggered by an initial socket.io event that is emitted via componentDidMount (get-menus).
CollapsibleList is collapsed/uncollapsed by its onclick which uses a toggleVisiblity method passed via props from the NestedList, whose state determines whether its child CollapsibleList components are open or not.
Problem: CollapsibleList props (which come from state of the NestedList) don't change on changing state of said NestedList. I have examined the properties in the debugger and I have been stuck for days. In other words, the CollapsibleList element appears on the browser window, but clicking it only changes the state of the NestedList, and the props of the CollapsibleList doesn't change, and thus it doesn't appear/disappear. I think it has something to do with creating the CollapsibleLists in the socket.io callback, bound with 'this', since the 'collapsed' prop of the CollapsibleList depends on this.state[restaurantId].collapsed. Source is below, if it is unclear I can add more explanation.
class CollapsibleList extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<List>
<ListItem
button
onClick={() => {
this.props.collapseEventHandler(this.props.restaurantId);
}}
>
<ListItemText primary="Collapse me!" />
</ListItem>
<ListItem>
<Collapse in={!this.props.collapsed} timeout="auto" unmountOnExit>
<ListItemText primary="Hello World!" />
</Collapse>
</ListItem>
</List>
);
}
}
class NestedList extends React.Component {
constructor(props) {
super(props);
//let menuData = props.menuData.map()
this.toggleVisiblity = this.toggleVisiblity.bind(this);
this.arrayOfMenus = [];
}
componentWillMount() {
socket.on(
"menu-arrived",
function(menuJson) {
if (menuJson.response.menu.menus.items) {
let restaurantId = menuJson.restaurant_id;
//let menuId = menuJson.response.menu.menus.items[0].menuId;
this.arrayOfMenus.push(
<CollapsibleList
collapsed={this.state[restaurantId].collapsed}
collapseEventHandler={this.toggleVisiblity}
restaurantId={restaurantId}
key={restaurantId}
/>
);
this.setState(function(prevState, props) {
return {
[restaurantId]: {
collapsed: prevState[restaurantId].collapsed,
updated: true
}
};
});
}
}.bind(this)
);
}
componentDidMount() {
getNearbyRestaurantRawData().then(
function(rawData) {
let restaurantIds = parseOutVenueIds(rawData);
let menusOpen = {};
for (let i = 0; i < restaurantIds.length; i++) {
menusOpen[restaurantIds[i]] = {
collapsed: true
};
}
this.setState(menusOpen, () => {
socket.emit("get-menus", {
ids: restaurantIds
});
});
}.bind(this)
);
}
toggleVisiblity(restaurantId) {
this.setState(function(prevState, props) {
let newState = Object.assign({}, prevState);
newState[restaurantId].collapsed = !prevState[restaurantId].collapsed;
return newState;
});
}
render() {
return (
<List>
<React.Fragment>
<CssBaseline>{this.arrayOfMenus}</CssBaseline>
</React.Fragment>
</List>
);
}
}
You are pushing CollapsibleList React elements to an array on the instance, which means that new React elements will not be created and returned from the render method when state or props change.
You should always derive your UI from state and props in the render method instead.
Example
class NestedList extends React.Component {
state = { restaurantIds: [] };
componentWillMount() {
socket.on("menu-arrived", menuJson => {
if (menuJson.response.menu.menus.items) {
let restaurantId = menuJson.restaurant_id;
this.setState(prevState => {
return {
restaurantIds: [...prevState.restaurantIds, restaurantId],
[restaurantId]: {
collapsed: prevState[restaurantId].collapsed,
updated: true
}
};
});
}
});
}
// ...
render() {
return (
<List>
<React.Fragment>
<CssBaseline>
{this.state.restaurantIds.map(restaurantId => (
<CollapsibleList
collapsed={this.state[restaurantId].collapsed}
collapseEventHandler={this.toggleVisiblity}
restaurantId={restaurantId}
key={restaurantId}
/>
))}
</CssBaseline>
</React.Fragment>
</List>
);
}
}

Categories

Resources