I'm having view with list of Steps. Each step can be "viewed" and "removed" from within the "view" screen. My StepDetails component binds to redux store by fetching corresponding step from steps part of store with simple steps.find(...):
const mapStateToProps = (state, ownProps) => {
let stepId = ownProps.params.stepId;
let step = state.steps.find(s => s.id === stepId);
return {step};
};
Now when (from within "details") I hit "Delete step" I want this step to be removed from store and I want to navigate to list view.
There is redux action invoked on delete that returns new steps list without this one removed and redirect is called afterwards:
const deleteStep = (stepId) => {
return dispatch => {
return stepsApi.deleteStep(stepId).then(steps => {
dispatch(action(STEPS_LIST_CHANGED, {steps}));
// react-router-redux action to redirectdispatch(redirectToList());
})
}
};
This is fine and does what I want, with one drawback: when STEPS_LIST_CHANGED action is called and step gets removed from the list, my component's mapStateToProps gets called before this redirect. Result is that mapStateToProps cannot obviously find this step anymore and my component gives me errors that step is undefined etc.
What I can do is to check if step provided to component is defined, and if not render nothing etc. But it's kind of defensive programming flavor I don't really like, as I don't want my component to know what to do if it gets wrong data.
I can also swap actions dispatch order: redirect first and then alter state, but then it doesn't feel right too (logically you first want to delete and then redirect).
How do you handle such cases?
EDIT: What I ended up with was to put this null/undefined-check into container component (one that does redux wiring). With that approach I don't clutter my presentational components with unnecessary logic. It also can be abstracted out to higher order component (or ES7 decorator probably) to render null or <div></div> when some required props are not present.
There are a few different ways to handle this situation. Here are a few suggestions:
Use a loading state: Instead of rendering an error message or nothing, you could render a loading state until the redirect happens. This would involve adding some additional state to your component (e.g. loading: true), and rendering a loading message or spinner until the componentDidUpdate lifecycle method is called with the new props after the redirect. At that point, you can set loading: false and render the component as normal.
Keep a reference to the step ID in your component state: Instead of relying on the ownProps.params.stepId value in your mapStateToProps function, you could store the ID in your component state when it is first passed in as a prop. Then, when the STEPS_LIST_CHANGED action is dispatched, you can update the component state with the new list of steps (which no longer includes the deleted step). This way, you can still access the original step ID even if it is no longer present in the updated state.steps array.
Move the redirect logic out of your action creator: Instead of dispatching the redirectToList() action from within your deleteStep action creator, you could move that logic to the component itself (e.g. in a componentDidUpdate lifecycle method that checks if the steps array has been updated). This way, your mapStateToProps function won't get called until after the redirect has happened.
Of these options, I would recommend either using a loading state or keeping a reference to the step ID in your component state. Both of these options should allow you to avoid cluttering your presentational components with defensive logic.