I have a higher order component which sets some values and then passes those as props to a wrappedComponent, however within that wrapped component when I access "this.props" from componentDidMount() the values are blank. If I place logs "this.props" from the render method in the wrappedComponent however I get the desired results, though i assume this is because of a re-render. What am i doing wrong here?
Home.js
import React, { Component } from 'react'
// eslint-disable-next-line
import { BrowserRouter as Router } from 'react-router-dom'
import { Route, Switch } from 'react-router-dom'
import BlogSummaryContainer from './utility/BlogSummaryContainer'
import BlogPost from './utility/BlogPost'
import EditableBlogPost from './utility/EditableBlogPost'
function withBlogPostData (WrappedComponent) {
return class BlogPostContainer extends React.Component {
constructor () {
super()
this.state = { title: '', content: '', catchPhrase: '' }
}
componentDidMount () {
fetch(`/api/posts/${this.props.match.params.id}`)
.then(res => {
return res.json()
})
.then(blogPost => {
// this setState doesnt reach the wrappedComponent in time even if i dont do a fetch and simply hard code a value, whats going on?
this.setState({
title: blogPost.title,
content: blogPost.content,
catchPhrase: blogPost.catchPhrase
})
})
}
render () {
return (
<WrappedComponent
id={this.props.match.params.id}
title={this.state.title}
content={this.state.content}
catchPhrase={this.state.catchPhrase}
/>
)
}
}
}
class Home extends Component {
... other code
render () {
return (
<Switch>
<Route
exact
path={`${this.props.match.url}`}
render={() => {
return <BlogSummaryContainer posts={this.state.blogPosts} />
}}
/>
<Route
exact
path={`${this.props.match.url}/:id`}
component={withBlogPostData(BlogPost)}
/>
<Route
exact
path={`${this.props.match.url}/:id/edit`}
component={withBlogPostData(EditableBlogPost)}
/>
<Route
exact
path={`${this.props.match.url}/new/post`}
render={() => {
return <EditableBlogPost isNew />
}}
/>
</Switch>
)
}
}
export default Home
EditableBlogPost.js
componentDidMount (props) {
const { title, catchPhrase, content } = this.props
console.log('this.props', this.props) // this.props = {title: "", content: "", ... }
}
I think this is just an asynchronous problem - when your HOC mounts it is calling fetch() which isn't resolved instantly so that is why on the first render this.state.x are their initial empty values.
When the Promise is resolved, the values are set and the subsequent render will have the expected values.
You could conditionally render to avoid rendering the wrapped component until the fetch() has resolved:
render () {
if(this.state.title.length === 0) {
return <div>Loading...</div>; //or some nice <Loading> component
}
return (
<WrappedComponent
id={this.props.match.params.id}
title={this.state.title}
content={this.state.content}
catchPhrase={this.state.catchPhrase}
/>
)
}
Related
I am using React to fetch an API to display data after clicking on buttons. Each button is a category, once clicked it shows cards for that category only.
I would like to know how to communicate between components to have one component with the buttons and one component with the data from one category without having to call the API twice and just passing the value for the category.
The value of the button is a dynamic value passed inside the API call and I am not sure how to communicate between the buttons components and the display of data.
Any help would be greatly appreciated
The routing component:
import React, { Component } from 'react';
import { Route } from 'react-router-dom';
import Navbar from '../components/NavBar/NavBar'
import HomeButtons from './HomeButtons/HomeButtons';
import CardsCategory from './CardsCategory/CardsCategory';
import CardsProps from './CardsCategory/CardsProps'
class Routing extends Component {
render() {
return (
<div>
<Route path="/" component={Navbar} />
<Route exact path="/" component={HomeButtons} />
<Route exact path="/cards" component={CardsProps} />
</div>
)
}
}
export default Routing
The buttons component :
import React, { Component } from 'react'
import axios from 'axios';
import './HomeButtons.css';
import ButtonCategory from '../../components/ButtonCategory/ButtonCategory'
class HomeButtons extends Component {
handleClick = (buttonValue) => {
// only 3 first letters of the button to match category in the API:
buttonValue = buttonValue.slice(0, 3).toLowerCase();
axios.get('http://api.nobelprize.org/2.0/nobelPrizes?sort=desc&nobelPrizeCategory=' + buttonValue + '&format=json&csvLang=en')
.then(res => {
const categoryData = res.data.nobelPrizes;
// console.log("CATEGORY", categoryData?.category?.en)
this.setState({
category: buttonValue
})
}).
catch(err => console.log(err))
};
render() {
const allCategoriesButtons = ["Physics", "Chemistry", "Medicine", "Literature", "Peace", "Economics"];
const allCatMap = allCategoriesButtons.map(button =>
< ButtonCategory
key={button.toString()}
value={button}
name={button}
onClick={e => this.handleClick(e.target.value)}
/>
)
return (
<div>
<div className="container__section">
{allCatMap}
</div >
</div>
)
}
}
export default HomeButtons;
The cards data component with a hard coded category in the API call :
import axios from 'axios';
import WinnerCard from '../../components/WinnerCard/WinnerCard'
class Cards extends Component {
state = {
allCards: []
}
componentDidMount() {
axios.get('http://api.nobelprize.org/2.0/nobelPrizes?sort=desc&nobelPrizeCategory=eco&format=json&csvLang=en')
.then(res => {
const categoryData = res.data.nobelPrizes;
this.setState({
allCards: categoryData,
// category: buttonValue
})
}).
catch(err => console.log(err))
};
render() {
const cards = this.state.allCards.map((card) => {
return <WinnerCard
key={card.id}
awardYear={card.awardYear}
category={card.category.en}
name={card.laureates[0].knownName?.en}
motivation={card.laureates[0].motivation?.en}
/>
})
return (
<div>
{cards}
</div>
)
}
}
export default Cards
Communication between SIBLING components can be achieved using FBEMITTER but that would IMHO make the structure a little confusing with multiple components interacting at multiple levels.
However, it feels like you might want to take a different approach something like below, just a thought.
Since you have routes defined for the cards tab, I think it's best, if you could send the category as a parameter in the URL itself, something like /cards/:category_name.
Changes go about like this
class Routing extends Component {
render() {
return (
<div>
<Route path="/" component={Navbar} />
<Route exact path="/" component={HomeButtons} />
<Route exact path="/cards/:category_name" component={Cards} />
</div>
)
}
}
export default Routing
HomeButtons
Here, instead of setting the state inside the axios then, you can just re route your application to /cards/:category_name
handleClick = (buttonValue) => {
// only 3 first letters of the button to match category in the API:
buttonValue = buttonValue.slice(0, 3).toLowerCase();
axios.get('http://api.nobelprize.org/2.0/nobelPrizes?sort=desc&nobelPrizeCategory=' + buttonValue + '&format=json&csvLang=en')
.then(res => {
---------------------------------------
Change the route to /cards/<buttonValue>
---------------------------------------
}).
catch(err => console.log(err))
};
Cards
class Cards extends Component {
...
componentDidMount() {
axios.get('http://api.nobelprize.org/2.0/nobelPrizes?sort=desc&nobelPrizeCategory=eco&format=json&csvLang=en')
.then(res => {
const categoryData = res.data.nobelPrizes;
this.setState({
allCards: categoryData,
category: <fetch category from the API that your router provides>
})
}).
catch(err => console.log(err))
};
render() {
...
}
}
export default Cards
I have already created a HOC in my react app following this, and its working fine. However i was wondering if there is a way to create a HOC as functional component(With or without state)??? since the given example is a class based component.
Tried to find the same over web but couldn't get anything. Not sure if thats even possible?? Or right thing to do ever??
Any leads will be appreciated :)
I agree with siraj, strictly speaking the example in the accepted answer is not a true HOC. The distinguishing feature of a HOC is that it returns a component, whereas the PrivateRoute component in the accepted answer is a component itself. So while it accomplishes what it set out to do just fine, I don't think it is a great example of a HOC.
In the functional component world, the most basic HOC would look like this:
const withNothing = Component => ({ ...props }) => (
<Component {...props} />
);
Calling withNothing returns another component (not an instance, that's the main difference), which can then be used just like a regular component:
const ComponentWithNothing = withNothing(Component);
const instance = <ComponentWithNothing someProp="test" />;
One way to use this is if you want to use ad-hoc (no pun intended lol) context providers.
Let's say my application has multiple points where a user can login. I don't want to copy the login logic (API calls and success/error messages) across all these points, so I'd like a reusable <Login /> component. However, in my case all these points of login differ significantly visually, so a reusable component is not an option. What I need is a reusable <WithLogin /> component, which would provide its children with all the necessary functionality - the API call and success/error messages. Here's one way to do this:
// This context will only hold the `login` method.
// Calling this method will invoke all the required logic.
const LoginContext = React.createContext();
LoginContext.displayName = "Login";
// This "HOC" (not a true HOC yet) should take care of
// all the reusable logic - API calls and messages.
// This will allow me to pass different layouts as children.
const WithLogin = ({ children }) => {
const [popup, setPopup] = useState(null);
const doLogin = useCallback(
(email, password) =>
callLoginAPI(email, password).then(
() => {
setPopup({
message: "Success"
});
},
() => {
setPopup({
error: true,
message: "Failure"
});
}
),
[setPopup]
);
return (
<LoginContext.Provider value={doLogin}>
{children}
{popup ? (
<Modal
error={popup.error}
message={popup.message}
onClose={() => setPopup(null)}
/>
) : null}
</LoginContext.Provider>
);
};
// This is my main component. It is very neat and simple
// because all the technical bits are inside WithLogin.
const MyComponent = () => {
const login = useContext(LoginContext);
const doLogin = useCallback(() => {
login("a#b.c", "password");
}, [login]);
return (
<WithLogin>
<button type="button" onClick={doLogin}>
Login!
</button>
</WithLogin>
);
};
Unfortunately, this does not work because LoginContext.Provider is instantiated inside MyComponent, and so useContext(LoginContext) returns nothing.
HOC to the rescue! What if I added a tiny middleman:
const withLogin = Component => ({ ...props }) => (
<WithLogin>
<Component {...props} />
</WithLogin>
);
And then:
const MyComponent = () => {
const login = useContext(LoginContext);
const doLogin = useCallback(() => {
login("a#b.c", "password");
}, [login]);
return (
<button type="button" onClick={doLogin}>
Login!
</button>
);
};
const MyComponentWithLogin = withLogin(MyComponent);
Bam! MyComponentWithLogin will now work as expected.
This may well not be the best way to approach this particular situation, but I kinda like it.
And yes, it really is just an extra function call, nothing more! According to the official guide:
HOCs are not part of the React API, per se. They are a pattern that emerges from React’s compositional nature.
Definitely you can create a functional stateless component that accepts component as an input and return some other component as an output, for example;
You can create a PrivateRoute component that accepts a Component as a prop value and returns some other Component depending on if user is authenticated or not.
If user is not authenticated(read it from context store) then you redirect user to login page with <Redirect to='/login'/>else you return the component passed as a prop and send other props to that component <Component {...props} />
App.js
const App = () => {
return (
<Switch>
<PrivateRoute exact path='/' component={Home} />
<Route exact path='/about' component={About} />
<Route exact path='/login' component={Login} />
<Route exact path='/register' component={Register} />
</Switch>
);
}
export default App;
PrivateRoute.jsx
import React, { useContext , useEffect} from 'react';
import { Route, Redirect } from 'react-router-dom'
import AuthContext from '../../context/auth/authContext'
const PrivateRoute = ({ component: Component, ...rest }) => {
const authContext = useContext(AuthContext)
const { loadUser, isAuthenticated } = authContext
useEffect(() => {
loadUser()
// eslint-disable-next-line
}, [])
if(isAuthenticated === null){
return <></>
}
return (
<Route {...rest} render={props =>
!isAuthenticated ? (
<Redirect to='/login'/>
) : (
<Component {...props} />
)
}
/>
);
};
export default PrivateRoute;
Higher Order Components does not have to be class components, their purpose is to take a Component as an input and return a component as an output according to some logic.
The following is an over simplified example of using HOC with functional components.
The functional component to be "wrapped":
import React from 'react'
import withClasses from '../withClasses'
const ToBeWrappedByHOC = () => {
return (
<div>
<p>I'm wrapped by a higher order component</p>
</div>
)
}
export default withClasses(ToBeWrappedByHOC, "myClassName");
The Higher Order Component:
import React from 'react'
const withClasses = (WrappedComponent, classes) => {
return (props) => (
<div className={classes}>
<WrappedComponent {...props} />
</div>
);
};
export default withClasses;
The component can be used in a different component like so.
<ToBeWrappedByHOC/>
I might be late to the party but here is my two-cent regarding the HOC
Creating HOC in a true react functional component way is kind of impossible because it is suggested not to call hook inside a nested function.
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function, before any early returns. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls. (If you’re curious, we’ll explain this in-depth below.)
Rules of Hooks
Here is what I have tried and failed
import React, { useState } from "react";
import "./styles.css";
function Component(props) {
console.log(props);
return (
<div>
<h2> Component Count {props.count}</h2>
<button onClick={props.handleClick}>Click</button>
</div>
);
}
function Component1(props) {
console.log(props);
return (
<div>
<h2> Component1 Count {props.count}</h2>
<button onClick={props.handleClick}>Click</button>
</div>
);
}
function HOC(WrapperFunction) {
return function (props) {
const handleClick = () => {
setCount(count + 1);
};
const [count, setCount] = useState(0);
return (
<WrapperFunction handleClick={handleClick} count={count} {...props} />
);
}
}
const Comp1 = HOC((props) => {
return <Component {...props} />;
});
const Comp2 = HOC((props) => {
return <Component1 {...props} />;
});
export default function App() {
return (
<div className="App">
<Comp1 name="hel" />
<Comp2 />
</div>
);
}
CodeSandBox
Even though the code works in codesandbox but it won't run in your local machine because of the above rule, you should get the following error if you try to run this code
React Hook "useState" cannot be called inside a callback
So to go around this I have done the following
import "./styles.css";
import * as React from "react";
//macbook
function Company(props) {
return (
<>
<h1>Company</h1>
<p>{props.count}</p>
<button onClick={() => props.increment()}>increment</button>
</>
);
}
function Developer(props) {
return (
<>
<h1>Developer</h1>
<p>{props.count}</p>
<button onClick={() => props.increment()}>increment</button>
</>
);
}
//decorator
function HOC(Component) {
// return function () {
// const [data, setData] = React.useState();
// return <Component />;
// };
class Wrapper extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
handleClick = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<Component count={this.state.count} increment={this.handleClick} />
);
}
}
return Wrapper;
}
const NewCompany = HOC(Company);
const NewDeveloper = HOC(Developer);
export default function App() {
return (
<div className="App">
<NewCompany name={"Google"} />
<br />
<NewDeveloper />
</div>
);
}
CodeSandbox
I think for functional component this works fine
import {useEffect, useState} from 'react';
// Target Component
function Clock({ time }) {
return <h1>{time}</h1>
}
// HOC
function app(C) {
return (props) => {
const [time, setTime] = useState(new Date().toUTCString());
useEffect(() => {
setTimeout(() => setTime(new Date().toUTCString()), 1000);
})
return <C {...props} time={time}/>
}
}
export default app(Clock);
You can test it here: https://codesandbox.io/s/hoc-s6kmnv
Yes it is possible
import React, { useState } from 'react';
const WrapperCounter = OldComponent =>{
function WrapperCounter(props){
const[count,SetCount] = useState(0)
const incrementCounter = ()=>{
SetCount(count+1)
}
return(<OldComponent {...props} count={count} incrementCounter={incrementCounter}></OldComponent>)
}
return WrapperCounter
}
export default WrapperCounter
import React from 'react';
import WrapperCounter from './WrapperCounter';
function CounterFn({count,incrementCounter}){
return(
<button onClick={incrementCounter}>Counter inside functiona component {count}</button>
)
}
export default WrapperCounter(CounterFn)
I have a React component called Home which is calling an action to fetch some groups when the component mounts.
I am calling an action as follows:
componentDidMount() {
const { fetchRecentGroups } = this.props;
fetchRecentGroups();
}
My reducer is picking up each action perfectly fine and is returning a state as follows:
switch(action.type) {
case REQUEST_GROUPS:
return {
...state,
loadState: FETCHING
};
case REQUEST_GROUPS_SUCCESS:
return {
...state,
loadState: SUCCESS,
groups: action.data.groups,
totalResults: action.data.totalResults
};
default:
return state;
}
I am also using the connect HOC on this component as follows:
const mapStateToProps = (state) => {
return {}
}
const mapDispatchToProps = (dispatch) => {
return {
fetchRecentGroups: () => {
dispatch(actions.fetchRecentGroups())
}
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Home);
The Home component is placed inside a Route like this:
<Route
exact={true}
path="/"
component={Home}
/>
My problem is that each time the reducer returns a state, the componentDidMount is called again and again in a loop. I would expect the mount to happen only once at the first load.
If I put componentDidUpdate and componentWillReceiveProps functions in my component, they are never called (only componentDidMount) so I am not able to compare props.
Does anyone know why this may be happening?
EDIT:
I have found my problem to be caused by this piece of code in my route:
const RouteBlock = () => {
if(errorSettings) {
return <Error {...errorSettings} />
}
return (
<div className={styles.RouteBlock}>
<Route exact={true} path="/" component={Home} />
<Route path="/search" render={() => <div>SEARCH</div>} />
</div>
);
};
return <Router><RouteBlock /></Router>
I changed it to:
return <Router>{RouteBlock()}</Router>
Every time you render your component, you are immediately calling function that is setting new state and you are triggering re-rendering of your component. Maybe you should use shouldComponentUpdate life cycle method that will check is your old state the same as new one.
Check out official docs: https://reactjs.org/docs/react-component.html#shouldcomponentupdate
A client request a feature to implement dashboard switching. I'm working on it:
Dashboard.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
// components
import UserDashboard from '../components/dashboard/user-dashboard/UserDashboard.js';
import NewUserDashboard from '../components/new-dashboard/user-dashboard/NewUserDashboard.js';
#connect((state) => {
return {
identity: state.identity.toJS().profile
};
})
export default class Dashboard extends Component {
render() {
const msisdn = this.props.location.state ? this.props.location.state.msisdn : null;
return (
<UserDashboard msisdn={ msisdn }/>
);
}
}
Dashboard.js is the dashboard controller. I have 2 dashboards: UserDashboard, and NewDashboard.
Let's say an user is viewing another screen, and in that screen there's a button. If that button is clicked, the Dashboard will call it's render method, returning NewDashboard instead. And NewDashboard will be automatically displayed. Is this possible?
Calling render method programmatically not possible.
You have to do state update of that particular component if you want to call render method of that component.
Say,if you want to call render method of Dashboard Component,you must call setState on this component. You can do some dummy state lifting for that.
Imagine you have this dashboard:
function DashBoard({index}) {
return index == 0 ? <UserDashBoard /> : <SecondDashBoard />;
}
Without a router:
class ParentComponent extends ReactComponent {
state = {
dashboardIndex: 0
}
changeDashboard() {
this.setState({
dashBoardIndex: (state.dashboardIndex + 1) % 2
})
}
render() {
return (
<div>
<button onclick={() => this.changeDashboard()}>Change dashboard</button>
<Dashboard index={this.state.dashboardIndex} />
</div>
)
}
}
With a router:
<Switch>
<Route match="/component1" component={UserDashboard} />
<Route match="/component2" component={SecondDashboard} />
</Switch>
Also you can use redux.
You can use conditional rendering using state.
You can keep track of currently active tab and use that state to render the desired component.
More often than not, in order to change page views, you would make use of Router. You can configure Routes corresponding to Dashboard
import UserDashboard from '../components/dashboard/user-dashboard/UserDashboard.js';
import NewUserDashboard from '../components/new-dashboard/user-dashboard/NewUserDashboard.js';
#connect((state) => {
return {
identity: state.identity.toJS().profile
};
})
export default class Dashboard extends Component {
render() {
const msisdn = this.props.location.state ? this.props.location.state.msisdn : null;
return (
<BrowserRouter>
<Route path="/dashboard/user" render={(props) => <UserDashboard msisdn={ msisdn } {...props}/>} />
<Route path="/dashboard/new" render={(props) => <NewUserDashboard msisdn={ msisdn } {...props}/>} />
</BrowserRouter>
);
}
}
and on button click you can use a link.
Or else you can conditionally render component based on state change
// components
import UserDashboard from '../components/dashboard/user-dashboard/UserDashboard.js';
import NewUserDashboard from '../components/new-dashboard/user-dashboard/NewUserDashboard.js';
#connect((state) => {
return {
identity: state.identity.toJS().profile
};
})
export default class Dashboard extends Component {
state = {
userDashboard: true
}
onToggle=(state)=> {
this.setState(prevState => ({
userDashboard: !prevState.userDashboard
}))
}
render() {
const msisdn = this.props.location.state ? this.props.location.state.msisdn : null;
return <div>{userDashboard? <UserDashboard msisdn={ msisdn }/>
: <NewUserDashboard msisdn={ msisdn }/>}
<button onClick={this.onToggle}>Toggle</button>
</div>
);
}
}
Probably something like:
class NewDashboard extends React.Component {
static triggerRender() {
this.forceUpdate();
}
// or
static altTriggerRender() {
this.setState({ state: this.state });
}
render() {...}
}
Force React Component Render
Though, it's better to show/hide other components by conditional rendering.
Update:
"This" is not accessible inside a static method. Ignore the code.
I've got a parent component with react-router, setup like this :
constructor(props){
super(props);
this.state = {
diner: false
};
this.updateFromInvite = this.updateFromInvite.bind(this);
}
updateFromInvite(Souper) {
this.setState({diner: Souper});
}
I can't figure out how to setup the route to have both URL parameters and be able to pass a function to update the parent's state from the children component...
<Route path="/Invitation/:NomParam1?/:NomParam2?"
component = {() => (<Invitation updateApp = {this.updateFromInvite} />)} />
I think it's the closest I got...
From children's component :
class Invite extends Component {
constructor(props){
super(props);
this.state = {
diner: this.props.match.params.NomParam1 ,
JSONInfo: this.props.match.params.NomParam2
};
}
componentDidMount() {
const { diner } = this.state;
const { JSONInfo } = this.state;
const { updateApp } = this.props;
updateApp(diner);
}
render() {
return (
<div className="Invite">
<div className="col-centered">
<VidPlay/>
</div>
</div>
);
}
}
export default Invite;
The component property of the route takes a component Class, not an instance of the component. I believe you are looking to use the render property, which takes a rendered component. Your visual component shouldn't be concerned with the routing details, so you can pass that in in the Route configuration like so:
<Route path="/Invitation/:NomParam1?/:NomParam2?"
render={({match}) => (
<Invitation
updateApp={this.updateFromInvite}
diner={match.params.NomParam1}
JSONInfo={match.params.NomParam2}
/>
)}
/>
Then, in the component, don't utilize state, as that's not really what it is for:
class Invite extends Component {
componentDidMount() {
const { diner, JSONInfo, updateApp } = this.props;
// Not exactly sure what is going on here... how you
// will use JSONInfo, etc
updateApp(diner);
}
render() {
return (
<div className="Invite">
<div className="col-centered">
<VidPlay/>
</div>
</div>
);
}
}
Also, I'm not exactly sure what the parent component is doing, and why it is passing both the route params and the function down to the child, only to have the child call it back... but that is probably out of the scope of the question.
Enjoy!
If finally got it (thanks to that answer and the official documentation):
I needed to add props as parameter of my render and
use it with {...props} inside the children element!
<Route path="/Invitation/:NomParam1?/:NomParam2?"
render={ (props) =>
(<Invitation updateApp = {this.updateFromInvite} {...props} />)
}
/>
With that, I have access to BOTH :
my custom props
generic props (match, location and history)