React: How do I use my consumer inside ComponentDidMount to change state? - javascript

So I am using React's context because I have to change a state in the opposite direction.
E.g.:
App.js (has state) <--- My Component (changes the state in App.js)
I know how to do this using an onClick event. However, I fail understanding how to do this in a componentDidMount(). I created a basic example to illustrate what I'm trying to achieve:
MyComponent.js
import { MyConsumer } from '../App.js';
export default class MyComponent extends Component {
componentDidMount() {
// TRYING TO CHANGE STATE IN COMPONENTDIDMOUNT
<MyConsumer>
{({ actions }) => actions.setMyState(true)}
</MyConsumer>
}
render() {
return (
<SearchConsumer>
{({ actions }) => {
return (
<div onClick={() => actions.setMyState(true)}>
My content
</div>
)
}}
</SearchConsumer>
)
}
}
App.js
export const SearchContext = createContext();
export const SearchProvider = SearchContext.Provider;
export const SearchConsumer = SearchContext.Consumer;
class App extends Component {
constructor (props) {
super (props)
this.state = {
setMyState: 0,
}
}
render(){
return(
<SearchProvider value={
{
actions: {
setMyState: event => {
this.setState({ setMyState: 0 })
},
}
}
}>
<Switch>
<Route
exact path='/' render={(props) => <MyComponent />}
/>
</Switch>
</SearchProvider>
)
}
}

If you're using react 16.6.0 or later and are using exactly one context consumer, then the simplest approach is to use contextType (note that that's singular, not plural). This will cause react to make the value available on this.context, which you can then use in lifecycle hooks. For example:
// In some other file:
export default MyContext = React.createContext();
// and in your component file
export default class MyComponent extends Component {
static contextType = MyContext;
componentDidMount() {
const { actions } = this.context;
actions.setMyState(true);
}
// ... etc
}
If you are on an older version and thus can't use contextType, or if you need to get values from multiple contexts, you'll instead need to wrap your component in another component, and pass the context in via a prop.
// In some other file:
export default MyContext = React.createContext();
// and in your component file
class MyComponent extends Component {
static contextType = MyContext;
componentDidMount() {
const { actions } = this.props;
actions.setMyState(true);
}
// ... etc
}
export default props => (
<MyContext.Consumer>
{({ actions }) => (
<MyComponent actions={actions} {...props} />
)}
</MyContext.Consumer>
);

I fixed my problem by an idea given thanks to Nicholas Tower's answer. Instead of using the contextType in React, I just passed my actions as a prop in a different component. This way I could still use everything of my consumer if I just pass it on as a prop.
class MyComponent extends Component {
componentDidMount() {
this.props.actions.setMyState(true);
}
// ... etc
}
export default class MyComponentTwo extends Component {
render(){
return(
<MyConsumer>
{({ actions }) => (
<MyComponent actions={actions}/>
)}
</MyConsumer>
)
}
);

Related

GlobalContext is not update in class component

I'm trying to create GlobalContext but when I update this inside Class Component it didn't work in Class Component it's showing value of globalState but it's not updating globalState via setGlobalState
GlobalContext
import React, { useState ,ReactNode} from 'react'
const initialMapContext: { globalState: any; setGlobalState: React.Dispatch<React.SetStateAction<any>> } = {
globalState: {},
// will update to the reducer we provide in MapProvider
setGlobalState: () => {},
};
const GlobalContext = React.createContext(initialMapContext );
interface Props { children?: ReactNode;}
export function GlobalProvider({children}:Props){
const [ globalState, setGlobalState ] = useState({name:'pranjal'});
return <GlobalContext.Provider value={{ globalState, setGlobalState }}>{children}</GlobalContext.Provider>;
}
export default GlobalContext
my code in classComponent is
static contextType = GlobalContext;
getData = async () =>{
const { globalState, setGlobalState } = this.context;
console.log(globalState); // pranjal
setGlobalState({name:"please login"});
console.log(globalState); // pranjal
// my rest code
}
but setGlobalState is not updating globalState value .
Although it works fine in the Functional component
Function.js
const { globalState, setGlobalState } = useContext(GlobalContext);
setGlobalState({name:'Please login'})
Rather than using static contextType = GlobalContext; , I would recommend you to use a Higher Order Component (HOC) which wraps a component with your GlobalContext.
Impementation:
GlobalContext
Export one HOC method called withGlobalContext as follows,
export const withGlobalContext = (Component) => (props) => (
<GlobalContext.Consumer>
{({ globalState, setGlobalState }) => (
<Component
globalState={globalState}
setGlobalState={setGlobalState}
{...props}
/>
)}
</GlobalContext.Consumer>
)
ClassComponent
Wrap the component with the HOC, to get the global context values as the props. And being available in the props, you can use it anywhere in the component, even outside render()
class ClassComponent extends Component {
componentDidMount() {
console.log(this.props.globalState)
console.log(this.props.setGlobalState)
}
render() {
return (
// Your JSX here
)
}
export default withGlobalContext(ClassComponent)
Also, I prefer exporting a custom hook, for using context in functional components, rather than using useContext
Implementation:
export function useGlobalContext() {
const context = useContext(GlobalContext)
if (context === undefined) {
throw new Error('You did something wrong')
}
return [context.globalState, context.setGlobalState]
}
Then in your functional component, use it like following:
function FunctionalComponent(){
const [globalState, setGlobalState] = useGobalContext()
return (
// Your JSX here
)
}
Cheers!

How to use a state from one class to another

So i'm currently working on a PokeDex using the PokeApi available online.
The code of the project is as follows:
import React, { Component } from "react";
import PokemonCard from "./PokemonCard";
import "../ui/PokemonList.css";
import axios from "axios";
export const PokemonList = class PokemonList extends Component {
state = {
url: "https://pokeapi.co/api/v2/pokemon/",
pokemon: null
};
async componentDidMount() {
const res = await axios.get(this.state.url);
this.setState({ pokemon: res.data["results"] });
console.log(res);
}
render() {
return <div></div>;
}
};
export const PokeList = () => {
return (
<React.Fragment>
{this.state.pokemon ? (
<section className="poke-list">
{this.state.pokemon.map(pokemon => (
<PokemonCard />
))}
</section>
) : (
<h1>Loading Pokemon</h1>
)}
</React.Fragment>
);
};
As you can see, I have declared a state in the PokemonList Component class, but then I try to call it further down within the variable PokeList. The issue is that the state is not being recognized in PokeList
(I get the error "TypeError: Cannot read property 'state' of undefined" )
How can I go about calling the state that's declared in the class above?
-------------------EDIT-------------------------------
Okay, so I realized something. I have a code for my Dashboard.js that displays my list. Code is as follows
import React, { Component } from "react";
import { PokeList } from "../pokemon/PokemonList";
export default class Dashboard extends Component {
render() {
return (
<div>
<div className="row">
<div className="col">
<PokeList />
</div>
</div>
</div>
);
}
}
When I change the code from PokeList to PokemonList. so it'd be
import React, { Component } from "react";
import { PokemonList } from "../pokemon/PokemonList";
export default class Dashboard extends Component {
render() {
return (
<div>
<div className="row">
<div className="col">
<PokemonList />
</div>
</div>
</div>
);
}
}
I think get a list of 20 pokemon from the Api from
console.log(this.state.pokemon);.
But since I'm not displaying PokeList on the dashboard, then none of the pokemon cards display.
Screenshot of console output
First of all functional components are stateless. If you need to maintain state use class components or hooks. You can't use the state of one component in another component, You have two options,
Create a parent-child relationship between those components
Use state management libraries(Redux, etc)
There's a little of confusion between your PokemonList and PokeList component. I believe that what you really are looking for is to have just one of those. If you mix the two, you can have a component that controls the view based on the state, in your case, the state is your Pokemon list.
I mixed the two here, so your render method renders "Loading Pokemon" until you get your response back from axios, then when the response is back, it gets that data, updates your state and the state update trigger a re-render.
import React, { Component } from "react";
import PokemonCard from "./PokemonCard";
import axios from "axios";
class PokemonList extends Component {
state = {
url: "https://pokeapi.co/api/v2/pokemon/",
pokemon: null
};
componentDidMount() {
axios.get(this.state.url).then(res => {
this.setState({ pokemon: res.data["results"] });
});
}
render() {
let pokemonList = <h1>Loading Pokemon</h1>;
const pokemons = this.state.pokemon;
if (pokemons) {
pokemonList = (
<section className="poke-list">
<ul>
{pokemons.map(pokemon => (
<PokemonCard pokemon={pokemon} />
))}
</ul>
</section>
);
}
return <React.Fragment>{pokemonList}</React.Fragment>;
}
}
export default PokemonList;
I also created a simple PokemonCard component where I list the result from the API, just to show you that that approach works.
import React from "react";
const pokemonCard = props => {
return (
<li key={props.pokemon.name}>
<a href={props.pokemon.url}>{props.pokemon.name}</a>
</li>
);
};
export default pokemonCard;
You can find the final code, with PokeList and PokemonList now combined into one component called PokemonList here:
Keep in mind that if your render function depends on a certain state, it's probably certain that you should have that state being managed in that component, or passed down from a parent component.
In your example, I noticed you set url inside your state. URL is really not something that will change. It's a constant,so you can easily remove that from your state and place it in a variable and just leave your pokemon list there.
For example:
const url = "https://pokeapi.co/api/v2/pokemon/";
state = {
pokemon: null
};
componentDidMount() {
axios.get(url).then(res => {
this.setState({ pokemon: res.data["results"] });
});
}
import React , { Component } from "react";
import axios from "axios";
//make it as class based component
export default class PokemonList extends Component {
state = {
url: "https://pokeapi.co/api/v2/pokemon/",
pokemon: null
};
async componentDidMount() {
const res = await axios.get(this.state.url);
this.setState({ pokemon: res.data["results"] });
console.log(res);
}
render() {
//check your data here
console.log(this.state.pokemon)
{/*pass data to child*/}
return <div> <PokeList data = { this.state } /> </div>;
}
};
//export this component
export const PokeList = (props) => {
//check your data is coming or not
console.log(props.data)
//access your data from props
return (
<React.Fragment>
{props.data.pokemon ? (
<section className="poke-list">
{props.data.pokemon.map(pokemon => (
pokemon.name
))}
</section>
) : (
<h1>Loading Pokemon</h1>
)}
</React.Fragment>
);
};
You need iterate your your pokelist passing the result from your componentDidMount function to your child component as a prop , then receive your prop in the child component here it's a working codesandbox iterating your pokemon names in the pokeList child component

Forward ref through React Router's withRouter HOC

I'd like to manage to focus on a component that I've wrapped with withRouter. However, when I give the component a ref, I get the a warning about assigning a ref to a stateless component. I'm assuming this is because the ref is being attached to the withRouter HOC and not my component, as it is stateful. My general set up looks like this:
// InnerComponent.js
class InnerComponent extends Component {
constructor(props) {
super(props);
}
}
export default withRouter(InnerComponent);
// App.js
class App extends Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return (
<Router>
<InnerComponent ref={this.myRef}>
</Router>
);
}
I see this question has been asked before, but never answered. I'm new to React so please forgive me if I'm missing something obvious. Thanks in advance.
EDIT: I'm fairly sure what I need is here: https://reacttraining.com/react-router/web/api/withRouter, in the wrappedComponentRef section of the withRouter docs, but I don't understand how to implement it.
Based on #Ranjith Kumar answer I came up with the following solution that:
Is a bit shorter/simpler (no need for class component or withRef option)
Plays a bit better in tests and dev tools
const withRouterAndRef = Wrapped => {
const WithRouter = withRouter(({ forwardRef, ...otherProps }) => (
<Wrapped ref={forwardRef} {...otherProps} />
))
const WithRouterAndRef = React.forwardRef((props, ref) => (
<WithRouter {...props} forwardRef={ref} />
))
const name = Wrapped.displayName || Wrapped.name
WithRouterAndRef.displayName = `withRouterAndRef(${name})`
return WithRouterAndRef
}
Usage is the same:
// Before
export default withRouter(MyComponent)
// After
export default withRouterAndRef(MyComponent)
HOC component to forward inner component refs with withRouter HOC
const withRouterInnerRef = (WrappedComponent) => {
class InnerComponentWithRef extends React.Component {
render() {
const { forwardRef, ...rest } = this.props;
return <WrappedComponent {...rest} ref={forwardRef} />;
}
}
const ComponentWithRef = withRouter(InnerComponentWithRef, { withRef: true });
return React.forwardRef((props, ref) => {
return <ComponentWithRef {...props} forwardRef={ref} />;
});
}
Usage
class InnerComponent extends Component {
constructor(props) {
super(props);
}
}
export default withRouterInnerRef(InnerComponent);
Finally, I have done this way! this will work for sure
import { withRouter } from 'react-router';
//Just copy and add this withRouterAndRef HOC
const withRouterAndRef = (WrappedComponent) => {
class InnerComponentWithRef extends React.Component {
render() {
const { forwardRef, ...rest } = this.props;
return <WrappedComponent {...rest} ref={forwardRef} />;
}
}
const ComponentWithRouter = withRouter(InnerComponentWithRef, { withRef: true });
return React.forwardRef((props, ref) => {
return <ComponentWithRouter {...props} forwardRef={ref} />;
});
}
class MyComponent extends Component {
constructor(props) {
super(props);
}
}
//export using withRouterAndRef
export default withRouterAndRef (MyComponent)
I think you could use React.forwardRef (https://reactjs.org/docs/forwarding-refs.html) like this:
// InnerComponent.js
class InnerComponent extends Component {
constructor(props) {
super(props);
}
}
export default withRouter(InnerComponent);
// App.js
const InnerComponentWithRef = React.forwardRef((props, ref) => <InnerComponent ref={ref} props={props} />);
class App extends Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return (
<Router>
<InnerComponentWithRef ref={this.myRef}>
</Router>
);
}
Note: Untested code!
I thing you can use withRef option available while exporting the component.
See the below sample to export
// InnerComponent.js
class InnerComponent extends Component {
constructor(props) {
super(props);
this.exampleMethod = this.exampleMethod.bind(this);
}
exampleMethod(){
}
}
export default withRouter(InnerComponent , { withRef: true });
and also try the below method to access the methods using reference
this.myRef.getWrappedInstance().exampleMethod()
A much easier way to do this:
// InnerComponent.js
class InnerComponent extends Component {
constructor(props) {
super(props);
}
}
export default withRouter(InnerComponent);
// App.js
import InnerComponentWithRouter, { InnerComponent } from '/InnerComponent'
class App extends Component {
private myRef: InnerComponent|null = null;
constructor(props) {
super(props);
}
render() {
return (
<Router>
<InnerComponentWithRouter wrappedComponentRef={(r: InnerComponent) => this.myRef = r} />
</Router>
);
}
}
Simplified the good answer by #pandaiolo a bit more. Uses wrappedComponentRef, which is already used by withRouter
function withRouterAndRef(WrappedComponent) {
const RoutableComponent = withRouter(WrappedComponent);
const RoutableComponentWithRef = React.forwardRef((props, ref) => (
<RoutableComponent {...props} wrappedComponentRef={ref} />
));
const name = WrappedComponent.displayName || WrappedComponent.name;
RoutableComponentWithRef.displayName = `withRouterAndRef(${name})`;
return RoutableComponentWithRef;
}

How to write a ReactJS high order component using ES6?

I´m trying to build a ReactJS high order component using ES6 syntax. Here is my try:
export const withContext = Component =>
class AppContextComponent extends React.Component {
render() {
return (
<AppContextLoader>
<AppContext.Consumer>
{context => <Component {...props} context={context} />}
</AppContext.Consumer>
</AppContextLoader>
);
}
};
Here, AppContextLoader gets context from database and provide it to the context, as:
class AppContextLoader extends Component {
state = {
user: null,
};
componentWillMount = () => {
let user = databaseProvider.getUserInfo();
this.setState({
user: user
});
};
render = () => {
return (
<AppContext.Provider value={this.state}>
{this.props.children}
</AppContext.Provider>
);
};
}
export default AppContextLoader;
And usage:
class App extends Component {
static propTypes = {
title: PropTypes.string,
module: PropTypes.string
}
render = () => {
return (
withContext(
<Dashboard
module={this.props.module}
title={this.props.title}
/>
);
export default App;
For some reason my wrapped component (Dashboard) is not getting my context property, just the original ones (title and module).
How to properly write HOC using ES6 syntax?
You are not using the HOC correctly, you need to pass the component and not the component instance. Also invoking the HOC from within render is a bad patten since each render a new component will be returned, you must write
const DashboardWithContext = withContext(Dashboard);
class App extends Component {
render = () => {
return (
<DashboardWithContext
module={"ADMIN"}
title={"MY APP"}
/>
)
}
}
export default App;
Also in withContext HOC since the returned component is a class, you would access props like {...this.props} instead of {...props}, However it makes sense to use a functional component since you aren't actually using the lifecycle methods
export const withContext = Component => (props) => (
<AppContext.Consumer>
{context => <Component {...props} context={context} />}
</AppContext.Consumer>
);
Working Codesandbox
It should be this.props instead:
<Component {...this.props}
This should be working for you:
render() {
const props = this.props;
return (
<AppContext.Consumer>
{context => <Component {...props} context={context} />}
</AppContext.Consumer>
);
}
You have a few problems:
You're not using the Context API properly - the context is created for the use of a Provider to share a value to one or many Consumers - you are creating with the hoc a new Provider and Consumer.
from your example, you don't need to use Context - use an hoc for new use data - withUserData
You should use this.props and not props
In the usage section, you pass to the hoc an element and not a component
You're not getting the props from withContext
Solution
export const withUserData = BaseComponent =>
class AppContextLoader extends Component {
state = {
user: null,
};
componentWillMount = () => {
let user = databaseProvider.getUserInfo();
this.setState({
user: user
});
};
render = () => {
return (
<BaseComponent {...this.props} {...this.state} />
);
};
}
And usage:
class App extends Component {
static propTypes = {
title: PropTypes.string,
module: PropTypes.string
}
render = () => {
return (
<EnhancedDashboard
module={this.props.module}
title={this.props.title}
/>
);
}
const EnhancedDashboard = withUserData(Dashboard)
export default App;

Cannot assign a ref to a component in a render prop

Suppose you have a simple react application (see my codesandbox):
import React from 'react';
import { render } from 'react-dom';
class RenderPropComponent extends React.Component {
render() {
console.log(this.example.test());
return this.props.render();
}
}
class Example extends React.Component {
test = () => console.log('Test successful!');
render() {
return <h1>I am an example!</h1>;
}
}
const App = () => (
<RenderPropComponent
render={() => {
return (
<Example ref={node => this.example = node} />
)
}}
/>
);
render(<App />, document.getElementById('root'));
This causes the error:
TypeError
Cannot read property 'test' of undefined
How can I assign a ref to a component rendered via render prop?
I know I can accomplish this with this.props.children as follows:
class RenderPropComponent extends React.Component {
const childComponents = React.Children.map(this.props.children, child => {
return React.cloneElement(child, {ref: node => this.example = node})
});
console.log(this.example.test());
render() {
return <div>{childComponents}</div>;
}
}
...
<RenderPropComponent>
<Example />
</RenderPropComponent>
But I would like to be able to use a render prop to do this! Any suggestions?
Not fully sure if it fits your case, but maybe you can pass the setRef function as an argument to the render prop? Like in this forked sandbox.
import React from 'react';
import { render } from 'react-dom';
const styles = {
fontFamily: 'sans-serif',
textAlign: 'center',
};
class Hello extends React.Component {
constructor(props) {
super(props);
this.setRef = this.setRef.bind(this);
}
setRef(node) {
this.example = node;
}
render() {
console.log(this.example && this.example.test());
return this.props.render(this.setRef);
}
}
class Example extends React.Component {
test = () => console.log('Test successful!');
render() {
return <h1>I am an example!</h1>;
}
}
const App = () => (
<div style={styles}>
<Hello
name="CodeSandbox"
render={(setRef) => {
return (
<Example ref={setRef} />
)
}}
/>
<h2>Start editing to see some magic happen {'\u2728'}</h2>
</div>
);
render(<App />, document.getElementById('root'));
The only problem i see here, is that on initial render this.example will not be available (that's why I've added a guard to the console log) and after it will be set, the rerender will not be triggered (since it's set on the instance and not in the state). If a rerender is needed, we can store the ref in the component state or force the rerender.
On the other hand, if you need a ref to be used in some event handler later on, that should do the trick without rerendering.
Look carefully, this keyword is used in the global scope, not in the scope of the example component.
const App = () => (
<RenderPropComponent
render={() => {
return (
<Example ref={node => this.example = node} />
)
}}
/>
);
If you didn’t spot it yet, take a look at that snippet:
class Foo {
constructor(stuffToDo) {
this.bar = ‘bar’;
this.stuffToDo = stuffToDo
}
doStuff() {
this.stuffToDo();
}
}
new Foo(() => console.log(this.bar)).doStuff();
This will log undefined, not bar, because this is derived from the current closure.

Categories

Resources