Is React context provider not mounted before child components? - javascript

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>
);

Related

React functional component not updating on setState from parent component

My React component uses apollo to fetch data via graphql
class PopUpForm extends React.Component {
constructor () {
super()
this.state = {
shoptitle: "UpdateMe",
popupbodyDesc: "UpdateMe"
}
}
render()
{
return (
<>
<Query query={STORE_META}>
{({ data, loading, error, refetch }) => {
if (loading) return <div>Loading…</div>;
if (error) return <div>{error.message}</div>;
if (!data) return (
<p>Could not find metafields :(</p>
);
console.log(data);
//loop over data
var loopedmetafields = data.shop.metafields.edges
console.log(loopedmetafields)
loopedmetafields.forEach(element => {
console.log(element.node.value)
if (element.node.value === "ExtraShopDescription"){
this.setState({
shoptitle: element.node.value
});
console.log(this.state.shoptitle)
}
if (element.node.value === "bodyDesc"){
this.setState({
popupbodyDesc: element.node.value
});
console.log(this.state.popupbodyDesc)
}
});
return (
<>
<AddTodo mkey="ExtraShopDesc" namespace="ExtraShopDescription" desc={this.state.shoptitle} onUpdate={refetch} />
<AddTodo mkey="body" namespace="bodyDesc" desc={this.state.popupbodyDesc} onUpdate={refetch} />
</>
);
}}
</Query>
</>
)
}
}
export default PopUpForm
Frustratingly the functional component renders before the state is set from the query. Ideally the functional component would only render after this as I thought was baked into the apollo library but seems I was mistaken and it seems to execute synchronous rather than asynchronous
As you can see I pass the props to the child component, in the child component I use these to show the current value that someone might amend
The functional component is here
function AddTodo(props) {
let input;
const [desc, setDesc] = useState(props.desc);
//console.log(desc)
useEffect( () => {
console.log('props updated');
console.log(props)
}, [props.desc])
const [addTodo, { data, loading, error }] = useMutation(UPDATE_TEXT, {
refetchQueries: [
'STORE_META' // Query name
],
});
//console.log(data)
if (loading) return 'Submitting...';
if (error) return `Submission error! ${error.message}`;
return (
<div>
<form
onSubmit={e => {
console.log(input.value)
setDesc(input.value)
e.preventDefault();
const newmetafields = {
key: props.mkey,
namespace: props.namespace,
ownerId: "gid://shopify/Shop/55595073672",
type: "single_line_text_field",
value: input.value
}
addTodo({ variables: { metafields: newmetafields } });
input.value = input.value
}}
>
<p>This field denotes the title of your pop-up</p>
<input className="titleInput" defaultValue={desc}
ref={node => {
input = node;
}}
/>
<button className="buttonClick" type="submit">Update</button>
</form>
</div>
);
}
Now I need this component to update when the setState is called on PopUpForm
Another stack overflow answer here gives me some clues
Passing the intial state to a component as a prop is an anti-pattern
because the getInitialState (in our case the constuctor) method is
only called the first time the component renders. Never more. Meaning
that, if you re-render that component passing a different value as a
prop, the component will not react accordingly, because the component
will keep the state from the first time it was rendered. It's very
error prone.
Hence why I then implemented useEffect however the console.log in useEffect is still "updateMe" and not the value as returned from the graphql call.
So where I'm at
I need the render the functional component after the the grapql call
and I've manipulated the data, this seems to be the best approach in terms of design patterns also
or
I need setState to pass/render the functional component with the new value
As an aside if I do this
<AddTodo mkey="ExtraShopDesc" namespace="ExtraShopDescription" desc={data.shop.metafields.edges[0].node.value} onUpdate={refetch} />
It will work but I can't always expect the value to be 0 or 1 as metafields might have already defined
I think there is a simpler way than using setState to solve this. You can for example use find like this:
const shopTitleElement = loopedmetafields.find(element => {
return element.node.value === "ExtraShopDescription"
})
const shopBodyElement = loopedmetafields.find(element => {
return element.node.value === "bodyDesc"
});
return (
<>
<AddTodo mkey="ExtraShopDesc" namespace="ExtraShopDescription" desc={shopTitleElement.node.value} onUpdate={refetch} />
<AddTodo mkey="body" namespace="bodyDesc" desc={shopBodyElement.node.value} onUpdate={refetch} />
</>
);

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}
</>
);

How to access ref of a component through a function in React Native?

I've imported a custom component into my screen and rendered it in the render() function. Then, created a ref to that custom component. Now, the render() function simply looks like this.
render() {
return (
<View>
<MyComponent ref={component => this.myComponent1 = component} />
<MyComponent ref={component => this.myComponent2 = component} />
<MyComponent ref={component => this.myComponent3 = component} />
</View>
)
}
Then, In the same screen file, I've created another function to access the state of my custom component. I wrote it like this.
myFunction = (ref) => {
ref.setState({ myState: myValue })
}
Then, I want to call that function for those separate components separately like this. (In the screen file)
this.myFunction(this.myComponent1)
this.myFunction(this.myComponent2)
this.myFunction(this.myComponent3)
But, it does not work. It gives me the following error.
null is not an object (evaluating 'ref.setState')
Actually what I need this myFunction to do is,
this.myComponent1.setState({ myState: myValue })
this.myComponent2.setState({ myState: myValue })
this.myComponent3.setState({ myState: myValue })
The state myState is in the component while I want to access it through the myFunction() in my screen file.
Can you please help me to solve this problem?
This is not good practice to setState of child component from parent component.
I am assuming you want to set some value to your child component's state by trying this approach.
You can keep these values in your local state and pass it to props and your child component will re-render / get updated value there.
class Component {
constructor() {
this.state = {
myValues: {
component1: "abc",
component2: "xyz",
component3: "123",
}
}
}
myFunction(componentName, newValue) {
this.setState({
myValues: {
...this.state.myValues,
[componentName]: newValue
}
})
}
render() {
return (
<View>
<MyComponent value={this.state.myValues.component1} />
<MyComponent value={this.state.myValues.component2} />
<MyComponent value={this.state.myValues.component3} />
</View>
)
}
};
First make sur that MyComponent is a component and not a stateless Component, then, to change states, try this :
myFunction = (ref) => {
ref.current.setState({ myState: myValue }
}
and of course , for it to work, your component need to be already mounts, If you try for example to call this in the constructor, it will not work
Inside your component, in the componentDidMount function please add
componentDidMount() {
this.props.refs(this)
}
setState(key, value){
this.setState({[key]:value})
}
and please change the ref param to refs
<MyComponent refs={component => this.myComponent1 = component} />
myFunction = (ref) => {
ref.setState('myState', 'myValue')
}

Parent update causes remount of context consumer?

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.

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>
)
}
}

Categories

Resources