2
votes

I have the following async action creator (using redux-thunk):

export const nextPupil = () => {
    return (dispatch, getState) => {
    const {activeSentence, sentencesList} = getState();
    //... some more code
    const nextActive = pupilList[nextIndex];

    dispatch({ type: "UPDATE_ACTIVE", payload: nextActive });
  }
};

However, I need this action to return a promise, since I need to wait for its completion in one of my components, before doing something else (see this question for a more detailed description of my problem)

How could I turn this action into a promise and still use redux-thunk? Thanks a lot in advance.

edit:

I am aware that I am trying to save the state of the input field locally in my component, i.e., not in Redux. This because if I were to bind the value of the input field directly to the redux store, it would not be editable anymore. However, I want the user to be able to edit the input field and then, on submit, save the edited value in my redux store. If I bind things directly, the user won't be able to change values / edit.

1

1 Answers

3
votes

I think you are misunderstanding the data flows through redux. I assume when you want to turn the action into a promise, you would want that promise to resolve when the redux state has been updated - this appears to be the case from your other question. This is not generally how redux is designed to work.

When you dispatch an action, the store will be updated, and then the props in your components that come from the store will update. All you need to do is use those props in your components, and whenever the props update, so will your component.

In your initial question your code was:

onSubmit(e) {
  e.preventDefault();
  this.props.nextSentence(); //this is async

  this.setState({ //this is not yet updating with the right values then
      inputField: this.props.activePupil.name
  });
}

The issue you have is that activePupil is part of your redux store, yet you are trying to store it within the state of your component as well. This is a bad idea because you no longer have a single source of truth for your state.

The better approach is to just use this.props.activePupil wherever it is needed, e.g.

 render() {
     return (
        <span>{this.props.activePupil.name}</span>
     );
 }

Now when you dispatch the action, as soon store is updated, your component will also update. If you need to then you can add a loading property to your store, so that you can indicate to the user that something is happening, and dispatch this from your thunk before doing any async work, and then revert loading when UPDATE_ACTIVE is dispatched.

export const nextPupil = () => {
    return (dispatch, getState) => {
      dispatch({ type: "PUPIL_UPDATING });
      const {activeSentence, sentencesList} = getState();
      //... some more code
      const nextActive = pupilList[nextIndex];

      dispatch({ type: "UPDATE_ACTIVE", payload: nextActive });
  }
};

Edit 1

It's taken a bit of thinking about the right approach to your issue below, but I think I have settled on a reasonable way to achieve it, while maintaining data integrity. It does involve keeping form input in your store, which not everyone is a fan of, but is not a problem for me.

Anyway... we need another reducer for handling user input - it will be very simple and respond to two actions - the "UPDATE_ACTIVE" action you already have, and an "NAME_UPDATED" action:

const updatedName = (state = '', action) => {
  switch (action.type) {
    case 'UPDATE_ACTIVE':
        return action.payload.name;
    case: 'NAME_UPDATED':
        return action.payload.updatedName;
    default:
        return state;
  }
};

Then we will need a simple action creator:

const inputChanged = updatedName => ({
     type: 'NAME_UPDATED',
     payload: { updatedName }
});

Now within our component we will use the newly created state and action creator to manage our input (just connect them in as per usual) - lots of code missing here but there should be enough to get the idea.

class Whatever extends React.Component {
    constructor() {
        super();
        this.handleChange= this.handleChange.bind(this);
    }

    handleChange(event) {
        this.props.inputChanged(event.target.value);
    }

    render() {
        return (
            <input type="text" value={this.props.updatedName} onChange={this.handleChange} />
        );
    }
}

I hope that makes sense!

Edit 2:

If you want to store the input on component state rather than within the redux store, then I think this approach should work. It uses the componentWillReceiveProps lifecycle hook - to set the component state i.e. the input value when the activePupil prop changes. This is called every time the props update, but may also be called when there aren't any prop changes, so we need some logic to decide whether to update the updatedPupilName prop. To achieve this we are comparing our current props.activePupil.name to the nextProps.activePupil.name and only updating the state if they are different. If you have an ID type property on your pupils this would probably be better suited for the comparison.

class Whatever extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            updatedPupilName: props.activePupil.name
        };
        this.handleChange= this.handleChange.bind(this);
    }

    componentWillReceiveProps(nextProps) {
        if (nextProps.activePupil.name !== this.props.activePupil.name) {
            this.setState({
                updatedPupilName: nextProps.activePupil.name
            });
        }
    }

    handleChange(event) {
        this.setState({
            updatedPupilName: event.target.value
        });
    }

    render() {
        return (
            <input type="text" value={this.state.updatedPupilName} onChange={this.handleChange} />
        );
    }
}