I have a small react app. In App.js I have layout Sidenav and Content area. The side nav is shown on some page and hid from others. When I go to some components with sidenav, sidenav flag is set by redux and render the component again, in the componentDidMount I have api call, and it is executed twice.
class App extends Component {
renderSidebar = () => {
const {showNav} = this.props;
return showNav ? (
<TwoColumns>
<Sidenav/>
</TwoColumns>) : null;
};
render() {
const {showNav} = this.props;
const Column = showNav ? TenColumns : FullColumn;
return (
<Row spacing={0}>
{this.renderSidebar()}
<Column>
<Route exact path="/measurements/:id/:token/:locale/measure"
component={MeasurementPage}/>
</Column>
</Row>
)
}
}
const mapStateToProps = state => ({
showNav: state.sidenav.showNav
});
export default connect(mapStateToProps)(App);
I tried to use shouldComponentUpdate to prevent the second API call
class MeasurementPage extends Component {
constructor(props) {
// This update the redux "showNav" flag and re-render the component
props.toggleSidenav(false);
}
shouldComponentUpdate(nextProps) {
return !nextProps.showNav === this.props.showNav;
}
componentDidMount() {
// This is executed twice and made 2 api calls
this.props.getMeasurement(params);
}
render() {
return <h1>Some content here</h1>;
}
}
const mapStateToProps = state => ({
showNav: state.sidenav.showNav
});
export default connect(mapStateToProps)(MeasurementPage);
Did someone struggle from this state update and how manage to solve it?
This props.toggleSidenav(false) might cause side effect to your component lifecycle. We use to do this kind of stuff inside componentWillMount and it has been depreciated/removed for a reason :). I will suggest you move it inside componentDidMount
class MeasurementPage extends Component {
constructor(props) {
// This update the redux "showNav" flag and re-render the component
// props.toggleSidenav(false); // remove this
}
shouldComponentUpdate(nextProps) {
return nextProps.showNav !== this.props.showNav;
}
componentDidMount() {
if(this.props.showNav){ //the if check might not necessary
this.props.toggleSidenav(false);
this.props.getMeasurement(params);
}
}
render() {
return <h1>Some content here</h1>;
}
}
The comparison should be
shouldComponentUpdate(nextProps) {
return !(nextProps.showNav === this.props.showNav)
}
The problem is that !nextProps.showNav negate showNav value instead of negating the role expression value, and that is why you need an isolation operator.
It's No call twice anymore.
componentDidMount() {
if (this.first) return; this.first = true;
this.props.getMeasurement(params);
}
Related
I have an app with redux and router where on the first load, all users are loaded. To this end, I've implemented a main component that loads the user when the component is mounted:
class Content extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.load();
}
render() {
return this.props.children;
}
}
The afterwards, if the user chooses to load the details of one user, the details are also obtained through the same lifehook:
class Details extends Component {
componentDidMount() {
this.props.getByUrl(this.props.match.params.url);
}
render() {
const { user: userObject } = this.props;
const { user } = userObject;
if (user) {
return (
<>
<Link to="/">Go back</Link>
<h1>{user.name}</h1>
</>
);
}
return (
<>
<Link to="/">Go back</Link>
<div>Fetching...</div>
</>
);
}
Now this works well if the user lands on the main page. However, if you get directly to the link (i.e. https://43r1592l0.codesandbox.io/gal-gadot) it doesn't because the users aren't loaded yet.
I made a simple example to demonstrate my issues. https://codesandbox.io/s/43r1592l0 if you click a link, it works. If you get directly to the link (https://43r1592l0.codesandbox.io/gal-gadot) it doesn't.
How would I solve this issue?
Summary of our chat on reactiflux:
To answer your question: how would you solve this? -> High Order Components
your question comes down to "re-using the fetching all users before loading a component" part.
Let's say you want to show a Component after your users are loaded, otherwise you show the loading div: (Simple version)
import {connect} from 'react-redux'
const withUser = connect(
state => ({
users: state.users // <-- change this to get the users from the state
}),
dispatch => ({
loadUsers: () => dispatch({type: 'LOAD_USERS'}) // <-- change this to the proper dispatch
})
)
now you can re-use withUsers for both your components, which will look like:
class Content extends Component {
componentDidMount() {
if (! this.props.users || ! this.props.users.length) {
this.props.loadUsers()
}
}
// ... etc
}
const ContentWithUsers = withUsers(Content) // <-- you will use that class
class Details extends Component {
componentDidMount() {
if (! this.props.users || ! this.props.users.length) {
this.props.loadUsers()
}
}
}
const DetailsWithUsers = withUsers(Details) // <-- same thing applies
we now created a re-usable HOC from connect. you can wrap your components with withUsers and you can then re-use it but as you can see, you are also re-writing the componentDidMount() part twice
let's take the actual load if we haven't loaded it part out of your Component and put it in a wrapper
const withUsers = WrappedComponent => { // notice the WrappedComponent
class WithUsersHOC extends Component {
componentDidMount () {
if (!this.props.users || !this.props.users.length) {
this.props.loadUsers()
}
}
render () {
if (! this.props.users) { // let's show a simple loading div while we haven't loaded yet
return (<div>Loading...</div>)
}
return (<WrappedComponent {...this.props} />) // We render the actual component here
}
}
// the connect from the "simple version" re-used
return connect(
state => ({
users: state.users
}),
dispatch => ({
loadUsers: () => dispatch({ type: 'LOAD_USERS' })
})
)(WithUsersHOC)
}
Now you can just do:
class Content extends Component {
render() {
// ......
}
}
const ContentWithUsers = withUsers(Content)
No need to implement loading the users anymore, since WithUsersHOC takes care of that
You can now wrap both Content and Details with the same HOC (High Order Component)
Until the Users are loaded, it won't show the actual component yet.
Once the users are loaded, your components render correctly.
Need another page where you need to load the users before displaying? Wrap it in your HOC as well
now, one more thing to inspire a bit more re-usability
What if you don't want your withLoading component to just be able to handle the users?
const withLoading = compareFunction = Component =>
class extends React.Component {
render() {
if (! compareFunction(this.props)) {
return <Component {...this.props} />;
}
else return <div>Loading...</div>;
}
};
now you can re-use it:
const withUsersLoading = withLoading(props => !props.users || ! props.users.length)
const ContentWithUsersAndLoading = withUsers(withUsersLoading(Content)) // sorry for the long name
or, written as a bit more clean compose:
export default compose(
withUsers,
withLoading(props => !props.users || !props.users.length)
)(Content)
now you have both withUsers and withLoading reusable throughout your app
I have these two components:
import { findDOMNode } from 'react-dom';
class Items extends Component {
constructor(props) {
super(props);
this.ref = React.createRef();
this.selectedItemRef = React.createRef();
}
componentDidMount() {
if (this.props.selectedItem) {
this.scrollToItem();
}
}
componentWillReceiveProps(nextProps) {
if (this.props.selectedItem !== nextProps.selectedItem) {
this.scrollToItem();
}
}
scrollToItem() {
const itemsRef = this.ref.current;
const itemRef = findDOMNode(this.selectedItemRef.current);
// Do scroll stuff here
}
render() {
return (
<div ref={this.ref}>
{this.props.items.map((item, index) => {
const itemProps = {
onClick: () => this.props.setSelectedItem(item.id)
};
if (item.id === this.props.selectedItem) {
itemProps.ref = this.selectedItemRef;
}
return <Item {...itemProps} />;
})}
</div>
);
}
}
Items.propTypes = {
items: PropTypes.array,
selectedItem: PropTypes.number,
setSelectedItem: PropTypes.func
};
and
class Item extends Component {
render() {
return (
<div onClick={() => this.props.onClick()}>item</div>
);
}
}
Item.propTypes = {
onClick: PropTypes.func
};
What is the proper way to get the DOM node of this.selectedItemRef in Items::scrollToItem()?
The React docs discourage the use of findDOMNode(), but is there any other way? Should I create the ref in Item instead? If so, how do I access the ref in Items::componentDidMount()?
Thanks
I think what you want is current e.g. this.selectedItemRef.current
It's documented on an example on this page:
https://reactjs.org/docs/refs-and-the-dom.html
And just to be safe I also tried it out on a js fiddle and it works as expected! https://jsfiddle.net/n5u2wwjg/195724/
If you want to get the DOM node for a React Component I think the preferred way of dealing with this is to get the child component to do the heavy lifting. So if you want to call focus on an input inside a component, for example, you’d get the component to set up the ref and call the method on the component, eg
this.myComponentRef.focusInput()
and then the componentRef would have a method called focusInput that then calls focus on the input.
If you don't want to do this then you can hack around using findDOMNode and I suppose that's why it's discouraged!
(Edited because I realized after answering you already knew about current and wanted to know about react components. Super sorry about that!)
I was reading the section on Don’t Mutate the Original Component. Use Composition from this link.
https://reactjs.org/docs/higher-order-components.html
I then reviewed a project I'm trying to build. At a high level, this is what my code looks like:
class Wrapper extends Component {
constructor(props) {
this.wrappedComponent = props.wrappedComponent;
}
async componentWillAppear(cb) {
await this.wrappedComponent.prototype.fetchAllData();
/* use Greensock library to do some really fancy animation on the wrapper <Animated.div> */
this.wrappedComponent.prototype.animateContent();
cb();
}
render() {
<Animated.div>
<this.wrappedComponent {...this.props} />
</Animated.div>
}
}
class Home extends Component {
async fetchAllData(){
const [r1,r2] = await Promise.All([
fetch('http://project-api.com/endpoint1'),
fetch('http://project-api.com/endpoint2')
]);
this.setState({r1,r2});
}
animateContent(){
/* Use the GreenSock library to do fancy animation in the contents of <div id="result"> */
}
render() {
if(!this.state)
return <div>Loading...</div>;
return (
<div id="result">
{this.state.r1.contentHTML}
</div>
);
}
}
export default class App extends Component {
render() {
return <Wrapper wrappedComponent={Home} />;
}
}
My questions are:
In my Wrapper.componentWillAppear(), I fire the object methods like this.wrappedComponent.prototype.<methodname>. These object methods can set it's own state or animate the contents of the html in the render function. Is this considered mutating the original component?
If the answer to question 1 is yes, then perhaps I need a better design pattern/approach to do what I'm trying to describe in my code. Which is basically a majority of my components need to fetch their own data (Home.fetchAllData(){then set the state()}), update the view (Home.render()), run some generic animation functions (Wrapper.componentWillAppear(){this.animateFunctionOfSomeKind()}), then run animations specific to itself (Home.animateContent()). So maybe inheritance with abstract methods is better for what I want to do?
I would probably actually write an actual Higher Order Component. Rather than just a component which takes a prop which is a component (which is what you have done in your example). Predominately because I think the way you have implemented it is a bit of a code smell / antipattern.
Something like this, perhaps.
class MyComponent extends React.Component {
constructor() {
super();
this.animateContent = this.animateContent.bind(this);
}
componentWillReceiveProps(nextProps) {
if (this.props.r1 !== nextProps.r1) {
this.animateContent();
}
}
componentDidMount() {
// do your fetching and state setting here
}
animateContent() {
// do something
}
render() {
if(!this.props.r1) {
return <div>Loading...</div>;
}
return (
<div id="result">
{this.props.r1.title}
</div>
);
}
}
const myHOC = asyncFn => WrappedComponent => {
return class EnhancedComponent extends React.Component {
async componentDidMount(){
const [r1, r2] = await asyncFn();
this.setState({ r1, r2 })
this.animateContent();
}
animateContent = () => {
// do some animating for the wrapper.
}
render() {
return (<WrappedComponent {...this.props} {...this.state} />)
}
}
}
const anAsyncExample = async () => {
const result = await fetch("https://jsonplaceholder.typicode.com/posts");
return await result.json();
}
const MyEnhancedComponent = myHOC(anAsyncExample)(MyComponent);
Here's a working JSFiddle so you can see it in use:
https://jsfiddle.net/patrickgordon/69z2wepo/96520/
Essentially what I've done here is created a HOC (just a function) which takes an async function and returns another function which takes and a component to wrap. It will call the function and assign the first and second result to state and then pass that as props to the wrapped component. It follows principles from this article: https://medium.com/#franleplant/react-higher-order-components-in-depth-cf9032ee6c3e
I stuck in bit weird situation, I am using ReactJS. I have header container, title bar, title container. Header container has navigation bar. On click of that it effects title bar. I am using react router for that navigation. I am using componentDidMount lifecycle method for that.
Problem with that it triggers only once when title container loads. So I used componentDidUpdate. But in that problem occured when I added title bar component to title container. So now my componentDidUpdate runing in infinite loop. I tried to use shouldComponentUpdate(nextProps, nextState) but I don't know what condition put to return it false.
export class TitleContainer extends React.Component {
componentDidMount() {
this.props.dispatch(fetchDetail(this.props.match.params.program_id))
}
componentDidUpdate(prevProps, prevState) {
this.props.dispatch(fetchDetail(this.props.match.params.id))
}
shouldComponentUpdate(nextProps, nextState){
console.log("current props",this.props)
console.log("next props",nextProps)
// if(this.props.name == nextProps.name)
// return false;
return true;
}
render() {
console.log("data in contaner", this.props)
return (
<div>
<Title name = { this.props.name }
/>
</div>
)
}
}
const mapStateToProps = (state) => {
console.log("update state", state)
return {
programProfileData: state.DetailReducer.Details,
name: state.DetailReducer.name
}
}
export default connect(mapStateToProps)(TitleContainer)
If I understand your problem you would like to fetch other data if you change the params?
If so I would just remount the whole component.
class TitleContainer extends React.Component {
componentDidMount() {
this.props.dispatch(fetchDetail(this.props.match.params.program_id))
}
render() {
console.log("data in contaner", this.props)
return (
<div>
<Title name = { this.props.name }
/>
</div>
)
}
}
const mapStateToProps = (state) => {
console.log("update state", state)
return {
programProfileData: state.DetailReducer.Details,
name: state.DetailReducer.name
}
}
export default connect(mapStateToProps)(TitleContainer)
I think you don't need to use componentUpdate. You navigate to your that address, react router will creates that component, and you can extract the match props.
In your header you can have other Links from the react router dom lib which will replace your existing component. If you click on a link, react router pushes that to the browser history and creates a new component and therefore the params are updated.
I'm new to react.js, just follow the tutorial. Here is my code. At first, i tried to use the class Component 'Greeting' to let it show different words after
clicked the button, but i don't know what's wrong, it doesn't rerender the element, and the construtor() method of Greeting only called once. The commented out code functional Component 'Greeting' works well. Not sure what's the difference :(
class GreetingGuest extends React.Component {
render() {
return (
<h3>hello Guest, Click login button !!! </h3>
);
}
}
class GreetingUser extends React.Component {
render() {
return (
<h3>You have logged in, welcome !!!</h3>
);
}
}
class Greeting extends React.Component {
constructor(props) {
super(props);
console.log('Greeting.state.is_logon = ', props.is_logon);
this.state = {is_logon: props.is_logon};
}
render() {
let welcome_msg = null;
if (this.state.is_logon) {
welcome_msg = <GreetingUser />;
}else {
welcome_msg = <GreetingGuest />;
}
return welcome_msg;
}
}
//function Greeting(props) {
// const is_logon = props.is_logon;
// if (is_logon) {
// return <GreetingUser />;
// }
// return <GreetingGuest />;
//}
class LoginComponent extends React.Component {
constructor(props) {
super(props);
this.state = {is_logon: false};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(prevState => ({
is_logon: !prevState.is_logon
}));
}
render() {
let button = null;
let greeting = null;
if (this.state.is_logon) {
button = (
<button onClick={this.handleClick}>Logout</button>
);
greeting = <Greeting is_logon={this.state.is_logon} />
}else {
button = (
<button onClick={this.handleClick}>Login</button>
);
greeting = <Greeting is_logon={this.state.is_logon} />
}
return (
<div>
{greeting}
{button}
</div>
);
}
}
ReactDOM.render(
<LoginComponent />,
document.getElementById('Login')
)
HTML:
<html>
<body>
<div id="Login"></div>
</body>
<html>
The reason the class component doesn't re render, is because you have stored the logged_in prop in state from the constructor, and the constructor is only called once. Also state can only be modified from within the component.
To fix this you have 2 options;
Use componentWillReceiveProps, and update the local state with the new logged_in prop.
componentWillReceiveProps(nextProps) {
if (nextProps.logged_in !== this.state.logged_in) {
this.setState({ logged_in: nextProps.logged_in });
}
}
Or; do not use state but use the prop directly.
render() {
let welcome_msg = null;
if (this.props.is_logon) {
welcome_msg = <GreetingUser />;
}else {
welcome_msg = <GreetingGuest />;
}
return welcome_msg;
}
Where I think you should use the latter, since the parent component already maintains state.
Well to be honest the answer which I posted previously was wrong. It was because the way you posted the question telling that everything works fine when function based component is added. Then I created a project using your code and figured out few issues in your code.
Firstly you are maintaining state locally outside redux here. You are passing down the state of the login from the parent LoginComponent to the child component called Greeting like this.
greeting = <Greeting is_logon={this.state.is_logon} />
This gets passed as a props to the child component which is Greeting in this case. Remember React uses one way data flow.
Now from that child component you can access the data using this.props as shown below. You don't need to maintain any local state what so ever there.
Do the following changes in your Greeting component.
constructor(props) {
super(props);
}
Then make sure you access the values from this.props instead of any local state object like this.
render() {
let welcome_msg = null;
if (this.props.is_logon) {
welcome_msg = <GreetingUser />;
}else {
welcome_msg = <GreetingGuest />;
}
return welcome_msg;
}
}
This solved the issue. Happy Coding !