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;
Related
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!
I needed to add a language change to the website, I used context api, but when I used Provider to pass the state it did not return anything
LocalSwitch location: src/components/LocalSwitch/LocalSwitch
Home location: src/pages/Home/Home.js
LocaleSwitch.js
export const Context = React.createContext(require('../../locales/eng.json'))
class LocaleSwitch extends Component {
state = {
locale:{}
}
_update = lang =>{
this.setState({
locale:require(`../../locales/${lang}.json`),
}, ()=>{console.log(`${lang}`) })
}
render = ()=> {
const {Provider} = Context;
return (
<Provider value={this.state}>
<Wrapper>
{icons.map(icon=>(
<img
key={icon}
src= {require(`../../assets/${icon}.png`)}
alt= {icon}
className="locale"
onClick={()=> this._update(icon)}
/>
))}
</Wrapper>
</Provider>
);
}
}
Home.js
import {Context} from "../../components/LocaleSwitch/LocaleSwitch";
class Home extends Component {
state = {
locale:{}
}
render = ()=> {
const {Consumer} = Context;
return (
<Consumer>
{({locale})=>(
<Wrapper>
<Header>
<Hero id="hero"/>
<HeaderTitle id="title">
{locale.titleP1} <br/> {locale.titleP2}
<p>{locale.subtitle}<br/>{locale.subtitle2}</p>
</HeaderTitle>
</Wrapper>
)}
</Consumer>
);
}
}
It doesn't look like you have a Provider somewhere as a parent for your Consumer in your Home.js. You probably want to wrap the context provider somewhere at the root of your app.
In this case the Home component would have to rendered inside of the LocaleSwitch component.
Try to add the Provider functionality in Home.js and you'll see what I mean.
const Locale = React.createContext('english')
class Home extends React.Component {
render() {
return (
<Locale.Provider>
<Locale.Consumer>
{
// Should print 'english'
(locale) => {locale}
}
</Locale.Consumer>
</Locale.Provider>
)
}
}
what you are effectively doing is
const Locale = React.createContext('english')
class LocaleSwitch extends React.Component {
render() {
return (
<Locale.Provider>
<div>I could be a locale consumer if i wanted to </div>
</Locale.Provider>
)
}
}
class Home extends React.Component {
render() {
// I have no provider
return (
<Locale.Consumer>
{
(locale) => {locale}
}
</Locale.Consumer>
)
}
}
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>
)
}
);
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;
}
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.