React callbacks with arguments in child components [Solved]


#1

On the homepage I’m printing a list of projects in a React/Redux application. The homepage is a container and I want both the projects list and project item to be just presentation components. I’m passing in a callback for removing a project into the project list. Before passing it to each project item I wanted to get the callback working. Shortened code:

// Container component
    class Homepage extends Component {

  onRemoveProject(params) {
    console.log('user ID', this.props.userData.user);
    console.log('Projects ', this.props.projectsData.projects);
    console.log('Given params ', params);
  }

  render() {
    return (
      <ProjectList removeProject={() => this.onRemoveProject.bind(this)}/>
    );
  }
}

// Presentation component
const ProjectList = ({ removeProject }) => (

  <div>
    <button type="button" onClick={removeProject('test')}>Remove</button>
  </div>
)

With it setup like this the method is called and the this.props context is binded correctly but the params argument is filled with the button information rather than the ‘test’ i passed it. What am I doing wrong here?

I’ve committed the broken build as a reference:
Homepage - https://github.com/georgelovegrove/GeorgeWebsite/blob/master/imports/client/components/homepage.js
Project List - https://github.com/georgelovegrove/GeorgeWebsite/blob/master/imports/client/components/project_list.js
Project Item - https://github.com/georgelovegrove/GeorgeWebsite/blob/master/imports/client/components/project_item.js


#2

onClick should be given a function. Here, you are passing the result of calling the removeProject function with the test parameter, which should be undefined.
Two remarks : 1) your code is too complicated 2) don"t use “this” when doing functionnal programming


#3

What do you mean by this? It’s too vague to be helpful.

Move the binding into the constructor like:
this.onRemoveProject = this.onRemoveProject.bind(this) ?


#4

Thanks for the reply, can you suggest how this should be approached? I can’t see how you can give it a function that will have access to the parameters from the presentational component as it won’t submit them as values like a form would. I obviously don’t want to pass props down to a child component and link it up with action creators as it should just be a presentation component which is what it was like in the previous git commit.

In regard to the remarks, which part of the code is too complicated? The console logs are just there for getting it to function before I call the action creator. Or do you mean somewhere else? And when shouldn’t you use ‘this’ as most examples use it in numerous places - do you mean a certain place where i’m using it? Again if it’s for the console logs that’s obviously temporary whilst trying to get the callback working.


#5

Solved it myself. Just used .bind twice. The first .bind is so that homepage function has access to the user and projects data that I didn’t want to pass down to a child to simply pass back up. The second .bind is on the child component where you pass null as the first argument and then whatever you want to add. Now the homepage onRemoveProject function has access to both it’s own properties and the ones binded in the child component that I can send off to an action creator.

// Container component
    class Homepage extends Component {

  onRemoveProject(params) {
    console.log('user ID', this.props.userData.user);
    console.log('Projects ', this.props.projectsData.projects);
    console.log('Given params ', params);
  }

  render() {
    return (
      <ProjectList onRemoveProject={this.onRemoveProject.bind(this)}/>
    );
  }
}

// Presentation component
const ProjectList = ({ onRemoveProject }) => (

  <div>
    <button type="button" onClick={onRemoveProject.bind(null, { test: 'test' })}> Remove </button>
  </div>
)

Live and learn!

For reference the latest commit now functions how I wanted it too - https://github.com/georgelovegrove/GeorgeWebsite/commit/adebb93c2a4e54c14d6b35aaa4389e77bd2a5b70


#6

This is what i would call too complicated :wink:
What is “this” providing you in this example ?

I would do it either like this :

const Homepage = (props)=>{
  const onRemoveProject = (params)=>{
    //do stuff with params
  }
  return (
    <ProjectList onRemoveProject={onRemoveProject.bind(null, {test:"test"})}/>
  );
};
const ProjectList = (props)=>{
  return (
    <div>
      <button type="button" onClick={onRemoveProject}> Remove </button>
    </div>
  );
};

or like this :

const Homepage = (props)=>{
  const onRemoveProject = (params)=>{
    //do stuff with params
  }
  return (
    <ProjectList onRemoveProject={onRemoveProject} stuff={"test"}/>
  );
};

const ProjectList = (props)=>{
  const {stuff} = props;
  return (
    <div>
      <button type="button" onClick={onRemoveProject.bind(null, stuff)}> Remove </button>
    </div>
  );
};

No “this”. I prefer the first version where the presentationnal component is really dumb, and just calls the function he is asked to call when onClick is triggered.


#7

I thought the convention was if it’s a container such as homepage then you should use class (e.g. - class Homepage extends Component) where as presentational components can simply use an arrow function (e.g. - const ProjectsList (params) => {} ) as it nests within any given container.

The other issue with that is the solution I want is to bind the index of the project that is going to be removed in the child component which it only has access to when the list is being mapped over. To add the information in the parent binding would mean the list needs to be mapped over in the homepage container which wouldn’t be ideal I think.


#8

If your component has internal state, you need to use class or React.createClass.
If it has no internal state (depends on the way you will get your data from), you can use a function.

I don’t get what you are saying. The index is added when the component is created, hence in his parent.
When you are creating the component, you are passing it his index. No need to do it in a separate step.