Skip to content
Advertisement

Is it wrong to use an action’s payload inside a component with react-redux?

I’d like to keep track of API requests that I make with react-redux. To do this I’d like to generate a request Id inside the action and pass that along to middleware and reducers through the payload. Then when I’m dispatching the action from my component I can capture the request Id and use it for updating the component as the request progresses.

Here’s some example code

State

export interface State {
  [requestId: number]: Request;
}
export interface Request {
  status: string;
  error?: string;
}

Action

export function createRequest(): Action {
  return {
    type: "request",
    payload: {
      requestId: Math.random () // Make a random Id here
    }
  };
}

Reducer

export function createRequestReducer(state: State): State {
  return {
    ...state,
    ...{ state.payload.requestId: { status: "pending" } }
  }; 
}

Component

interface props {
  getRequestById: (id: number) => Request;
  createRequest: () => number;
}

const component = (props: testProps): JSX.Element => {
  const getRequestById = props.getRequestById;
  const [requestId, setRequestId] = useState(null);
  const [request, setRequest] = useState(null);

  useEffect(() => {
    if (requestId !== null) {
      setRequest(getRequestById(requestId));
    }
  }, [requestId]);

  return <div>The request status is {(request && request.status) || "Not started"}</div>;
}

function mapStateToProps(state: State) {
  return {
    getRequestById: (requestId: number): Request => {
      getRequestById(state, requestId)
    }
  };
}

function mapDispatchToProps(dispatch: Dispatch) {
  return {
    createRequest: (): number => {
      const action = createRequest();
      dispatch(action);
      return action.payload.requestId;
    }
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(component);

I expect this will work but it may be a massive anti pattern. Is this not advised and, if so, is there an alternative?

Advertisement

Answer

I think your approach works technically totally fine. Only “logically” it might make sense to make a some changes:

Yes, the “action” is something that is supposed to be sent to the reducer (and not used anywhere else, although there is technically no problem with that).

But what you can do:

1. separate action and values

Inside the action creator function, you can do whatever you want.

So you can create and use the action and the requestId seperately. This is technically exact the same as what you did, but logically separated.

E.g.:

function createRequest(){
    const requestId = createUniqueId();
    const action = { type: "request", payload: { requestId: requestId } };
    return {
        requestId: requestId, // <-- request id independent of the action
        action: action,       // <-- action independent of the request id
    };
}

function mapDispatchToProps( dispatch: Dispatch ){
  return {
    createRequest: (): number => {
      const { requestId, action } = createRequest();
      dispatch( action );    // <-- action independent of the request id
      return requestId;      // <-- request id independent of the action
    }
  };
}

2. “action dispatchers”

I (and apparently others as well) like to use what I call “action dispatchers”. This is an extra step and more code, but I think when you got used to this concept, it eliminates any doubts where code like that has to be put.

E.g.:

// Create the action, and nothing else:
const createRequestActionCreator = function( requestId ){
  return { type: "request", payload: { requestId: requestId } };
};

// Preper some data needed to create the action:
const createRequestActionDispatcher = function( dispatch ){
  return function(){
    const requestId = createUniqueId();
    dispatch( createRequestActionCreator( requestId ) );
    return requestId;
  };
};

// 
function mapDispatchToProps( dispatch: Dispatch ) {
  return {
    createRequest: (): number => {
      const requestId = createRequestActionDispatcher( dispatch )();
      return requestId;
    }
  };
}

2.a

Additionally you could pass such an “action dispatcher” directly as a prop, if you want. In this case it basically replaces your function in mapDispatchToProps, but is reusable, e.g.:

function mapDispatchToProps( dispatch: Dispatch ) {
  return {
    createRequest: createRequestActionDispatcher( dispatch ),
  };
}

2.b

Some people prefer to use a fat-arrow-function here, which I find more confusing, not less, but it looks cleaner as soon as you got used to that pattern:

const createRequestActionDispatcher = (dispatch: Dispatch) => (maybeSomeValue: MyType) => {
    const requestId = createUniqueId();
    dispatch( createRequestActionCreator( requestId ) );
    return requestId;
};

Remark:

I generally prefer to be consistent, for which I should always (or never) use these “action dispatchers”, but I found that most of the time I don’t need one, but sometimes I find them very useful. So I’m actually using dispatch( myAction ) in some places and myActionDispatcher(value)(dispatch) in others. I don’t like that, but it works well, and I don’t have a better idea.

User contributions licensed under: CC BY-SA
3 People found this is helpful
Advertisement