2

I am working on a show page based on the match.params from react router 4. For some reason, some functions runs 3 times which gives me the first 2 times undefined and the third time the result I want.

So I have this Show component which have the props "projects" from the main component. In this show component I do a find function on the array with the props projects inside of it. This must return a 1 item with the project.id equel to match.params.id. This is going good, and If I visit the page by clicking on a button (through the application) I get the page with the right project and properties (e.g. project.name). But If I do a reload or visit the page by using an url in the browser, I get an error saying property name is undefined. If I look in the console I see that my console.logs are fired 3 times. Like I said, the first 2 are undefined and the third is giving me the result I want. I guess the first 2 are resulting in the error I get.

My Show component

import React from 'react'
import { Button } from 'reactstrap'

const Show = ({match, projects}) => {

    let project = projects.find((project) => {
        //Return project with the id equel to the match.params.id
        return project.id == match.params.id;
    });

    //Both these console.logs runs 3 times when visiting a link by url
    console.log(project); //this one gives me 2 times undefined, third time is the right item
    console.log(match.params.id)

    return(

        <div className="container p-40">

            <div className="projects-header">

                {/* If I echo project.name it will gives me 'cannot read property 'name' of undefined'  */}
                <h2>Project '{match.params.id}' {/* {project.name} */}</h2>

                <Button className="btn btn-primary" onClick={() => console.log('save')}>Save</Button>

            </div>

            <div className="project-edit">



            </div>

        </div>

    );

};

export default Show;

The route in the parent component

<Route exact path="/projects/show/:id" render={(props) => <Show {...props} projects={this.state.projects} />} />

Someone with a good solution for this problem? I find it a problem that my users can't route by the url. I don't know why my show component fires 3 times either by url/reload.

Edit (Added parent component):

//Import react
import React, { Component } from 'react';

//Import custom components
import Sidebar from './components/js/Sidebar'
import Dashboard from './components/js/Dashboard'
import Projects from './components/js/Projects'
import Show from './components/js/projects/Show'

//Import styles
import './App.css';


//3rd party deps
import { BrowserRouter as Router, Route } from "react-router-dom";
import axios from 'axios'


class App extends Component {

  constructor() {
    super();

    this.state = {
      //Times / Time tracking
      times: [],
      timer: false,
      currentTimer: 0,

      //Current task
      currentTask: {
        id: 3,
        title: '',
        project_id: {
          id: '',
          name: '',
          color: ''
        },
        date: '',
        time_total: ''
      },

      //Projects
      projects: []

    }

    this.addTask = this.addTask.bind(this);
    this.startTimer = this.startTimer.bind(this);
    this.stopTimer = this.stopTimer.bind(this);
    this.addProject = this.addProject.bind(this);

  }

  addTask = (task) => {

    let newArray = this.state.times.slice();

    newArray.push(task);

    this.setState({times: newArray, currentTimer: 0, timer: false});

    clearInterval(this.timerID);

  }

  addProject = (project) => {

    let newArray = this.state.projects.slice();

    newArray.push(project);

    this.setState({ projects: newArray });

  }


  startTimer() {

    let sec = this.state.currentTimer;

    const start = Date.now();

    this.setState({ timer: true });

    this.timerID = setInterval(() => {


      let time = new Date() - (start - sec * 1000);

      this.setState({ currentTimer: Math.round(time / 1000)});


    }, 1000);

  }

  stopTimer() {

    this.setState({ timer: false });
    console.log('stopped');

    clearInterval(this.timerID);
    //Clear interval here

  }

  componentDidMount() {

    // Make a request for a user with a given ID
    axios.get('/Sample.json')
      .then((response) => {

        this.setState({times: response.data});

    });

    axios.get('/Projects.json')
      .then((response) => {

        this.setState({projects: response.data});

    });


  }

  render() {
    return (

      <Router>

        <div className="page-wrapper">

          <Sidebar />

          <Route exact path="/" render={() => <Dashboard times={this.state.times} timer={this.state.timer} startTimer={this.startTimer} stopTimer={this.stopTimer} currentTimer={this.state.currentTimer} addTask={this.addTask} />} />
          <Route exact path="/projects" render={() => <Projects projects={this.state.projects} addProject={this.addProject} />} />
          <Route exact path="/projects/show/:id" render={(props) => <Show {...props} projects={this.state.projects} />} />

        </div>

      </Router>


    );
  }
}

export default App;
Giesburts
  • 6,879
  • 15
  • 48
  • 85
  • Is the parent component firing multiple times? For example are you setting state in the parent component which causing a re-render and causing child components to re-render as well? It may be helpful to show the parent component – Stretch0 Mar 07 '18 at 09:56
  • Hmm good point! I will update my question so you can take a look. I think in my lifecycle method `componentDidMount`. – Giesburts Mar 07 '18 at 09:59
  • have updated my answer with an example of how to check if variable is set – Stretch0 Mar 07 '18 at 10:22

2 Answers2

2

Looks as though when your component first mounts, you don't have the data required to render your app completely.

You are making your API call which sets state which calls a re-render. You are then passing the state projects to your show component. So when your show component first tries to render, it doesn't have access to the projects data but then when the API call has finished and the final render occurs, you have your projects data.

Wrap your find function in some conditional checking to see if projects is set

Something as simple as:

const Show = ({match, projects}) => {

    if(!projects) reutrn null

    let project = projects.find((project) => {
        //Return project with the id equel to the match.params.id
        return project.id == match.params.id;
    });

...
Stretch0
  • 8,362
  • 13
  • 71
  • 133
  • `if(!project) return null` will lead to a view without data, because if project doesn't exist (the first call already) it will return null and won't even go on the other calls/re-renders I guess. – Giesburts Mar 07 '18 at 10:22
  • @Gijsberts Only on the first render, will it return null. When your API call finishes and sets state, it will cause the children to re-render in which case, projects will be set and return a view with data. I meant `if(!projects)` – Stretch0 Mar 07 '18 at 10:25
2

You are making a couple of async requests in your componentDidMount of App.js and hence when you reload, both of these requests are fired and can return the response at different times, so two setState calls are triggered resulting in render being called 3 times(1 at initial render and 2 after setStates).

At the initial render, projects is an empty array, so you get undefined the first time in child after find.

It may so happen that Sample.json load after it, causing a setState call, at this point too, projects is an empty array and hence project is undefined in child

Now when the Projects.json is loaded, projects array contains array and project will be defined

So you need to add a conditional check for project prop

return(

        <div className="container p-40">

            <div className="projects-header">

                {/* If I echo project.name it will gives me 'cannot read property 'name' of undefined'  */}
                <h2>Project '{match.params.id}' {project && project.name} */}</h2>

                <Button className="btn btn-primary" onClick={() => console.log('save')}>Save</Button>

            </div>

            <div className="project-edit">



            </div>

        </div>

    );

In case you have multiple properties that you want to use from project, you could destructure them like

const { name, color } = project || {}

P.S. Note that this only works for one level nesting

Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
  • Alright cool. Just one question, what if I have more properties I try to access? Like `project.color` AND `project.name`. Do I have to include in every bracket `project` like `project && project.name` and `project && project.color`? Or is there an easier way of doing this? – Giesburts Mar 07 '18 at 10:17
  • Updated the answer – Shubham Khatri Mar 07 '18 at 10:20