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
Related
I'm currently learning React/hooks/redux. To do so, I'm building a react app that takes in data from a climate API.
The problem I'm having is correctly setting state for a couple of items in useEffect. One state relies on the other, so I'm trying to figure out how to properly call useEffect so I don't get infinite loops and follow best-practices.
A little background before the code included below:
-The user creates a project, and selects a city. This produces a cityId that I'm storing in my "project" state.
-On the user's dashboard, they can click a project that sends the project ID in a queryString to my ClimateData component.
-ClimateData passes the project ID queryString to the "getProjectByID" redux action to get the project state, including it's cityId.
-ClimateData includes the IndicatorList component, which brings in a list of all the climate data breakouts. I want the user to click one of these list items and have ClimateData's "indicatorByCityData" state set. So I passed ClimateData's setState function to IndicatorList and have the list call with onClicks. Is there a better way I should do this?
-On ClimateData, once I have the project's cityId, and the selected item from IndicatorList, I need to call "getIndicatorByCity" and pass both the cityId and indicator to have the result saved in the "indicatorByCityData" state
I keep trying to change how my ClimateData's useEffect is written, but I'm either getting infinite loops or errors. How can I best change this to set both states and follow best practices?
The redux actions and reducers have been tested elsewhere and work fine, so for brevity, I'll exclude them here and just focus on my ClimateData and IndicatorList components:
import React, { Fragment, useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import IndicatorList from './IndicatorList';
import Spinner from '../layout/Spinner';
import { getProjectById } from '../../actions/projects';
import { getIndicatorByCity } from '../../actions/climate';
const ClimateData = ({
getProjectById,
getIndicatorByCity,
project: { project, loading },
auth,
match
}) => {
const [indicatorByCityData, setIndicatorByCityData] = useState({});
const nullProject = !project;
useEffect(() => {
if (!project) getProjectById(match.params.id);
// Once we have the cityID, set the indicatorByCityData state, with a default selected Indicator
if (!loading) setIndicatorByCityData(getIndicatorByCity(project.cityId));
}, []);
// Get the selected indicator from IndicatorList and update the indicatorByCityData state
const setIndicator = indicator => {
setIndicatorByCityData(getIndicatorByCity(project.cityId, null, indicator));
};
return (
<Fragment>
{project === null || loading || !indicatorByCityData ? (
<Spinner />
) : (
<Fragment>
<Link to='/dashboard' className='btn btn-light'>
Back To Dashboard
</Link>
<h1 className='large text-primary'>{`Climate Data for ${project.city}`}</h1>
<IndicatorList setIndicator={setIndicator} />
</Fragment>
)}
</Fragment>
);
};
ClimateData.propTypes = {
getProjectById: PropTypes.func.isRequired,
getIndicatorByCity: PropTypes.func.isRequired,
project: PropTypes.object.isRequired,
auth: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
project: state.projects,
auth: state.auth
});
export default connect(mapStateToProps, { getProjectById, getIndicatorByCity })(
ClimateData
);
/******************************************************************/
import React, { useEffect, Fragment } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import Spinner from '../layout/Spinner';
import { getIndicatorList } from '../../actions/climate';
const IndicatorList = ({
getIndicatorList,
auth: { user },
climateList: { indicatorList, loading },
setIndicator
}) => {
useEffect(() => {
getIndicatorList();
}, [getIndicatorList]);
return loading ? (
<Spinner />
) : (
<Fragment>
{indicatorList.length > 0 ? (
<Fragment>
<ul>
{indicatorList.map(indicator => (
<li key={indicator.name}>
<a href='#!' onClick={() => setIndicator(indicator.name)}>
{indicator.label}
</a>
<br />- {indicator.description}
</li>
))}
</ul>
</Fragment>
) : (
<h4>No climate indicators loaded</h4>
)}
</Fragment>
);
};
IndicatorList.propTypes = {
auth: PropTypes.object.isRequired,
climateList: PropTypes.object.isRequired,
setIndicator: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
auth: state.auth,
climateList: state.climate
});
export default connect(mapStateToProps, { getIndicatorList })(IndicatorList);
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)
Using Next.js / React.js I have build a simple app using the setup as shown in the official Next.js tutorial. This means that I am using a <Layout /> component which is rendered around the different pages. I show my code below.
Now, using render props I try to set some state of the <Layout /> component from a child component. When doing this I get the following error:
TypeError: setLoginToken is not a function
Can someone explain why this is happening, and show me how to get this to work? I get this error when clicking one of the two buttons on
My code:
Layout component (components/Layout.js)
import React from "react";
class Layout extends React.Component {
state = {
loginToken: "abc123"
};
setLoginToken = newToken => {
this.setState({ loginToken: newToken });
};
render() {
const { render } = this.props;
const { loginToken, setLoginToken } = this.state;
return (
<React.Fragment>{render({ loginToken, setLoginToken })}</React.Fragment>
);
}
}
export default Layout;
index page (pages/index.js)
import React from "react";
import Layout from "../components/Layout";
class Index extends React.Component {
render() {
return (
<Layout
render={({ loginToken, setLoginToken }) => (
<div>
<p>Login token: {loginToken}</p>
<button onClick={() => setLoginToken("asdfasdf")}>Log in</button>
<button onClick={() => setLoginToken("")}>Logout</button>
</div>
)}
/>
);
}
}
export default Index;
I have also tried the following, with the same result:
(setLoginToken) => setLoginToken("asdfasdf")
(setLoginToken) => setLoginToken("")
The issue come from that you're destructuring setLoginToken from this.state, instead of this.
You can one shot this with
const { state: { loginToken }, setLoginToken } = this
I have a component in my app that renders some data, most commonly a page title.
Markup:
<Toolbar>
<ToolbarRow>
<div id="title-bar">
{children}
</div>
</ToolbarRow>
</Toolbar>
How would I declaratively be able to change the data inside?
I've tried react-side-effects which allowed me to indeed change the title to be rendered but then I wanted to be able to add components as well.
Components aren't to be stored inside state so there's that…
Then I looked at Portals, which seem to exactly what I want but I get Target container is not a DOM element.
Markup for the portal component:
import React from "react";
import {createPortal} from 'react-dom'
const PageTitle = ({title, children}) => {
return createPortal(
<p>foo</p>,
document.getElementById('title-bar')
)
};
export default PageTitle;
I'm calling the portal component like so:
<PageTitle title="Document Overview"/>
As you can see from the above snippet, the other component adds a <div id="title-bar" />, so I guess it has to do with timing.
Anyone have a good idea?
I would just put components into the state here:
const bars = [];
export class TitleBar extends Component {
state = { children: [] };
componentDidMount() { bars.push(this); }
componentWillUnmount() { bars.splice(bars.indexOf(this), 1); }
render() { return this.state.children };
}
const RealPageTitle = ({ title }) => <div> { title } </div>;
export class PageTitle extends Component {
constructor(props) {
super(props);
this.real = RealPageTitle(props);
}
componentDidMount() {
for(const bar of bars)
bar.setState(({ children }) => ({ children: children.concat(this.real) }));
}
componentWillUnmount() {
for(const bar of bars)
bar.setState(({ children }) => ({ children: children.filter(child => child !== this.real) }));
}
render() { }
}
That way you can just add <PageTitle title={"Test"} /> somewhere on the page and it gets added to the title bar.
I know this does not follow "best practices", but it certainly works
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.