Dispatching Actions from Child Components

Diganta Das
ITNEXT
Published in
7 min readMar 13, 2018

--

In a React-Redux setup, we have containers which connects to store using connect annotation. The container contains views or other child components. The child component can go down n-levels from the view. And it is often required for child components to dispatch Actions.

I have discussed 4 ways to achieve the same:

  1. Direct Approach (Not so correct way… )
  2. Pass the dispatch function as props (Ok, but still not such a good idea.. )
  3. Passing callback functions to child (Almost there, but things get messy later on…)
  4. Single callback function with enums (My proposed solution… )

Direct Approach

So to dispatch Actions from any components, you need access to the dispatch function. So the first thing comes to mind is lets put the connect annotation in the child component, and dispatch the Actions.

Calling actions directly from child components

However, there are a few problems:

  1. The child component becomes a smart-component.
  2. Reusing the child components becomes difficult.
  3. All child component needs to be aware of the Actions types and redux store structure
  4. Giving each component access to store is not a proper react-redux architecture.

Pass the dispatch function as props

Passing the dispatch function from containers to child function can solve the problem of making each of our child components as smart components.

MyContainer.jsx

@connect(store => ({ myStore: store.myStore }))
class MyContainer extends Component {
render() {
return (
<MyComponent
dispatch={this.props.dispatch}
/>
}
}

MyComponents.jsx

class MyComponent extends Component {
static propTypes = {
dispatch: PropsTypes.func,
}
onButtonClick = () = {
this.props.dispatch(MyActions.BUTTON_CLICKED, data);
}
render() {
return (
<div>
<button onClick={this.onButtonClick}>
// implement component view
</div>
)
}
}
Passing dispatch down to all child and call actions directly

In this approach, we no longer need to declare the child component as a smart component, and the component can be reused, however it still fails to solve the other problems:

  1. Child component still needs to know about the redux structure and action types.
  2. Parent will have no control of the functionality of the child.

Passing callback functions to child components

Keeping good practices in mind, we should call all Actions from containers only. To achieve this we can pass callback functions as props to child components. So each child component will have a list of callback functions defined in there prop-types. The parent can pass function references to the child for each of them.

MyContainer.jsx

@connect(store => ({ myStore: store.myStore }))
class MyContainer extends Component {
onTaskStart = (payload) => {
this.props.dispatch(MyActionTypes.TASK_START, payload);
}
onTaskEnd = (payload) => {
this.props.dispatch(MyActionTypes.TASK_END, payload);
}
render() {
return (
<MyComponent
onTaskStart={this.onTaskStart}
onTaskEnd={this.onTaskEnd}

/>
}
}

MyComponent.jsx

class MyComponent extends Component {
static propTypes = {
onTaskStart: PropsTypes.func,
onTaskEnd: PropTypes.func,
}
render() {
return (
<div>
// implement component view
</div>
)
}
}
Passing callback functions for each actions separately down to the child from container

This gives us some advantages:

  1. The child component can clearly specify all the tasks or functions it exposes to parent by it’s prop-types.
  2. The child need not worry about the functionality of actions or which actions to call.
  3. The parent can choose to implement or ignore certain functionalities.
  4. The child component can enforce implementation of certain functionalities by using prop-type as required.

Although this is sometimes the preferred method of approach for most developers, you will soon come across some obvious disadvantages:

  1. As functionalities increases, passing so many function references from containers to child components looks bad.
  2. When child component goes more than one or two levels, imagine passing all the function reference props through all the levels.
  3. Adding one functionality in one inner child component required changing all the components from container to that child component files. For any medium size project container to child can easily go more than 3 levels, and by this time you are already banging your head or given up.

Single callback function with enums

Looking in the previous implementations I want to come up with a solution which have the advantages of a single function callback like the dispatch, as well as giving control to the parent, and at the same time not making too much code change to add new functionality in any child components. And this is how I implement it:

MyContainer.jsx

@connect(store => ({ myStore: store.myStore }))
class MyContainer extends Component {
callbackHandler = (type, data) => {
switch(type) {
// will come to back to this later on
}
}
render() {
return (
<MyMainView
callbackHanlder={this.callbackHandler}
/>
}
}
}

MyContainer connects to store and get access to dispatch function. Instead of passing the dispatch function itself it passes a function reference callbackHandler().

MyMainView.jsx

import { 
CALLBACK_ENUMS,
}, ChildComponent from './MyChildComponent.jsx';
// merged list of all possible enums, also from child
const VIEW_CALLBACK_ENUMS = {
...CALLBACK_ENUMS,
MAIN_VIEW_TASK: 'MY_MAIN_VIEW/MAIN_VIEW_TASK',
};
class MyMainView extends Component { // handle only the requst from the child compoenents
// it either choose to bubble to the parent or choose to stop the request
// it can manipulate the data passed to parent if required.

viewCallBackHandler = (type, data) => {
switch(type) {
case CALLBACK_ENUMS.CHILD_MAIN_TASK:
// manipulate data if required
this.props.callbackHandler(type, data);
break;
case CALLBACK_ENUMS.CHILD_SECONDARY_TASK:
// choose to ignore this action
break;
default:
// bubble up all other actions to parent
this.props.callbackHandler(type, data);
}
};
onViewButtonClick = () => {
this.props.callbackHandler(
VIEW_CALLBACK_ENUMS.MAIN_VIEW_TASK,
data,
);
};
render() {
return (
<div>
<Button
onClick={this.onViewButtonClick}
>
Main Container Button
</Button>
<ChildCompoenent
callbackHandler={this.viewCallBackHandler}
// callbackHandler={this.props.callbackHandler}
// if parent need not filter out child's requests
/>
</div>
)
}
}
export default MyMainView;
export {
VIEW_CALLBACK_ENUMS as CALLBACK_ENUMS,
};

MyMainView can contain child components as well as it’s own view with functionality. The viewCallbackHandler acts as a intermediate filter between the child and the parent container. It is optional and can be removed. In that case the callbackHandler from the props can be directly passed to the child. This filter function can manipulate data before passing to parent or even choose to ignore certain request. Any requests which are not explicitly handled in this function is passed to parent by default. All request can be uniquely identified by the type which is an enum, described in the component itself.

Another thing to notice over here is how the VIEW_CALLBACK_ENUMS merges all enums from the child as well as its own. This ensures that all the enums from all child component from all levels gets merged and get passed to the parent container.

MyChildComponent.jsx

// list of all possible enums in child
const VIEW_CALLBACK_ENUMS = {
CHILD_MAIN_TASK: 'MY_CHILD/CHILD_MAIN_TASK',
CHILD_SECONDARY_TASK: 'MY_CHILD/CHILD_SECONDARY_TASK',
};
class MyChildComponent extends Component { onMainButtonClick = () => {
this.props.callbackHandler(
VIEW_CALLBACK_ENUMS.CHILD_MAIN_TASK,
data,
);

};
onSecondaryButtonClick = () => {
this.props.callbackHandler(
VIEW_CALLBACK_ENUMS.CHILD_SECONDARY_TASK,
data,
);

};
render() {
return (
<div>
<Button
onClick={this.onMainButtonClick}
>
Child Main Button
</Button>
<Button
onClick={this.onSecondaryButtonClick}
>
Child Secondary Button
</Button>
</div>
)
}
}
export default MyChildComponent;
export {
VIEW_CALLBACK_ENUMS as CALLBACK_ENUMS,
};

MyChildComponent simply have its own set of enums that it exports. And all actions are handled by callbackHandler function from the props. The child need not know how these actions are handled by the parent, nor it need to be aware of the redux structure or any actions types.

MyChildComponent is simply a reusable view component and all its actions are exposed by the function callbackHandler passed as props, and all the functionalities types are exported as enums.

Now lets get back to MyContainer.jsx callbackHandler function:

import { CALLBACK_ENUMS }, MyMainView from './MyMainView.jsx';// ...callbackHandler = (type, data) => {
switch(type) {
case CALLBACK_ENUMS.CHILD_MAIN_TASK:
this.props.dispatch(MyActions.childMainTask(data);
break;

case CALLBACK_ENUMS.MAIN_VIEW_TASK:
this.props.dispatch(MyActions.childMainTask(data);
break;
}
}
Passing a single callback function from container down to all child

You wanna know advantages?

  1. The child components are now dumb components and need not be aware of the redux structure or the action types.
  2. Child components can be easily reused anywhere as they don’t depend on any strict action types.
  3. If you want to add a functionality to a child 3 levels down, all you need to change is in the child and the main container. All intermediate levels need not be bothered as long as they don’t want to filter or manipulate the action data.
  4. Parent component can choose to have a filter for any preprocessing or blocking actions from child.
  5. Parent container can keep track of all actions passed from it’s view.
  6. Parent container have access to all the enums from all its child from all levels. So we can easily get the idea what all functionalities this view have implemented.
  7. All actions finally get dispatched from container only. World is finally all right again!!!

Conclusion

This approach worked perfectly for me in a medium size project. Especially towards the ends when adding new functionality can get really messy, this approach only required to change in two files. It keeps all your action calls in one place and keeps the code clean with enums and properly exposed functionalities for all child components.

--

--