Search code examples
javascriptreactjsreact-props

This.props undefined when component re-renders


I've been having a problem in this component with props. At first props were undefined when mounted but after refreshing the page it worked.

I read that it was because the data was fetched after mounting, so I fixed it with a conditional in the render. However, now I have the opposite error and I cannot find any solution that it works for me. Props work ok when mounting but when refreshing I get undefined.

Anyone has any idea why this is happening?

Here is my code: Profile

import React from "react";
import axios from "axios";
import { Card, CardBody, CardTitle, CardText} from "reactstrap";
import Withuser from "./Withuser"

class Profile extends React.Component {
  constructor(props) {
    super(props)
    console.log(props)
    this.state = {
      thoughts: [],
    }
  }

  componentDidMount = () => {
    this.getShares()
  }

  getShares = () => {
    console.log(this.props.user[0].id)
    const user_id = this.props.user[0].id

    axios(`http://localhost:7001/api/profile/shares/${user_id}`)

    .then(res => {
      console.log(res.data)
      this.setState(state => ({
        thoughts: res.data,
        loggedIn: !state.loggedIn
      }))
    })
    .catch(error => {
      this.setState({ error: true })
    })
  }

  render() {
    const { thoughts } = this.state
    if (!thoughts.length === 0) {
      return <div />
    }
    return(
    <div>
      <h1>Your posts</h1>
      <ul>
        {thoughts.map((thought, index) => {
          return (
            <Card className='thoughts' key={index}>
              <CardBody>
                <CardTitle>{thought.user_name} posted at {thought.createdAt}</CardTitle>
                <CardText>{thought.body}</CardText>
              </CardBody>
            </Card>
          )
        })}
      </ul>
    </div>
    ) 
  }
}


export default Withuser(Profile);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

Whithuser

import React, { useState, useEffect } from 'react'
import axios from 'axios'

const withUser = (Component, options = { renderNull: true }) => props => {
  const [userData, setUserData] = useState(null)
  const [userId, setUserId] = useState(null)
  const [error, setError] = useState(false)
  //const [loading, setLoading] = useState(false)

  useEffect(() => {
    const token = localStorage.getItem('token')
    if (!token) return
    //setLoading(true)
    axios('http://localhost:7001/api/profile', {
      headers: {
        'x-access-token': token,
      },
    })
      .then(response => {
        const id = response.data.id
        setUserId(id)
      })
      .catch(error => {
        setError(true)
        console.log(error)
      })
      {/*}.finally(() => {
        setLoading(false)
      })*/}
  }, [])

  useEffect(() => {
    //setLoading(true)
    axios(`http://localhost:7001/api/users/${userId}`)
      .then(response => {
      
      setUserData(response.data)
    })
    {/*}.finally(() => {
      setLoading(false)
    })*/}
  }, [userId])
  
  //if(loading) return null;
  if (!userData && options.renderNull) return null
  return <Component {...props} user={userData} />
}

export default withUser
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>


Solution

  • Here is how I would refactor this code.

    First, inside your withUser HOC, instead of two useEffect hooks, I would combine the work into a single useEffect that is why you are getting an initial render with props.user.id is undefined.

    I would also clean up the axios requests into async functions for readability.

    withUser

    import React, { useState, useEffect } from 'react';
    import axios from 'axios';
    
    const fetchProfile = async () => {
      const token = localStorage.getItem('token');
    
      if (!token) {
        throw new Error('Missing Token');
      }
    
      const response = await axios('http://localhost:7001/api/profile', {
        headers: {
          'x-access-token': token,
        },
      });
    
      const profile = response.data;
    
      return profile;
    };
    
    const fetchUsers = async (userId) => {
      const response = await axios(`http://localhost:7001/api/users/${userId}`);
      const users = response.data;
      return users;
    };
    
    const withUser = (Component, options = { renderNull: true }) => (props) => {
      const [userData, setUserData] = useState();
    
      useEffect(() => {
        async function loadUser() {
          try {
            const profile = await fetchProfile();
            const users = await fetchUsers(profile.id);
            setUserData(users);
          } catch (error) {
            console.error(error);
          }
        }
        loadUser();
      }, []);
    
      if (userData === undefined && options.renderNull === true) {
        return null;
      }
    
      return <Component {...props} user={userData} />;
    };
    
    export default withUser;
    

    Then in the Profile component, I wouldn't change much, other than refactoring getShares() into an async function. And then a little cleanup here and there.

    Profile

    import React from 'react';
    import axios from 'axios';
    import { Card, CardBody, CardTitle, CardText } from 'reactstrap';
    import withUser from './Withuser';
    
    class Profile extends React.Component {
      constructor(props) {
        super(props);
    
        this.state = {
          error: false,
          loggedIn: undefined,
          thoughts: [],
        };
      }
    
      componentDidMount = () => {
        this.getShares();
      };
    
      getShares = async () => {
        const userId = this.props.user[0].id;
    
        try {
          const response = await axios(`http://localhost:7001/api/profile/shares/${userId}`);
          this.setState((state) => ({
            thoughts: response.data,
            loggedIn: !state.loggedIn,
          }));
        } catch (error) {
          this.setState({ error: true });
        }
      };
    
      render() {
        const { thoughts } = this.state;
    
        if (!(thoughts.length > 0)) {
          return null;
        }
    
        return (
          <div>
            <h1>Your posts</h1>
            <ul>
              {thoughts.map((thought, index) => {
                return (
                  <Card className="thoughts" key={index}>
                    <CardBody>
                      <CardTitle>
                        {thought.user_name} posted at {thought.createdAt}
                      </CardTitle>
                      <CardText>{thought.body}</CardText>
                    </CardBody>
                  </Card>
                );
              })}
            </ul>
          </div>
        );
      }
    }
    
    export default withUser(Profile);