Giter VIP home page Giter VIP logo

Comments (10)

jeffbski avatar jeffbski commented on August 11, 2024 3

@jktravis Since redux doesn't currently provide a way to know when a particular action has completed, I'd recommend the following approach.

I would put all of the real work that happens for an UPDATE_ACCOUNT into a function that returns a promise when it is done (or rejects with an error if any problems).

/* API.updateAccount */
function updateAccount(id, value) {
  // update the value and recursively call updateAccount() on the parent(s)
  // return a promise that completes when everything is done
  return promise;
}

So all the real work is done in the API and you can easily test and call this in isolation.

Now then we will just wrap this with a small logic wrapper to use it in redux-logic.

You didn't mention exactly how you want the actions to be used, but I'll assume:

  • UPDATE_ACCOUNT - triggers an updateAction, payload: { id: 100, value: 123 }
  • UPDATE_ACCOUNT_COMPLETE - indicate when everything is updated, payload: id
  • DELETE_ACCOUNT - trigger a deleteAction, payload: id
  • DELETE_ACCOUNT_COMPLETE - indicate when done, payload: id
  • ERROR - you could also have specific types of error actions
const updateAccountLogic = createLogic({
  type: UPDATE_ACCOUNT, // run when we see this action type
  process({ getState, action }) {
    const { id, value } = action.payload; // get id and value
    return updateAccount(id, value)
      .then(() => ({ type: UPDATE_ACCOUNT_COMPLETE, payload: id }))
      .catch(err => {
         console.error(err); // good to do in case of render err
         return { type: ERROR, payload: err, error: true }
      });
  }
});
// given that we did not include dispatch in the process signature, it will use the 
//  return value for dispatching or specifically in this case the resolved/rejected 
//  value of the returned promise.

// OR if using processOptions we could simplify this as

const updateAccountLogic = createLogic({
  type: UPDATE_ACCOUNT, // run when we see this action type
  processOptions: {
    successType: UPDATE_ACCOUNT_COMPLETE, // dispatch action on success
    failType: ERROR // dispatch action on error/reject
  },
  process({ getState, action }) {
    const { id, value } = action.payload; // get id and value
    return updateAccount(id, value)
      .then(() => id); // return id to be used for payload
  }
});

Then I would suggest that you would also create an API deleteAccount(id) which calls your updateAccount internally, then you can test all of this in isolation.

/* API deleteAccount */
function deleteAccount(id) {
  return updateAccount(id, 0)
    .then(() => {
      // do my additional delete stuff here
      // return promise that resolves when the delete is done
      return promise;
    });
}

That makes our redux-logic wrapper for deleteAccount equally simple.

const deleteAccountLogic = createLogic({
  type: DELETE_ACCOUNT,
  process({ getState, action }) {
    const id = action.payload;
    return deleteAccount(id)
      .then(() => ({ type: DELETE_ACCOUNT_COMPLETE, payload: id }))
      .catch(err => {
         console.error(err); // in case of render error
         return { type: ERROR, error: err, error: true };
      });
  })
});

// OR if using processOptions

const deleteAccountLogic = createLogic({
  type: DELETE_ACCOUNT,
  processOptions: {
    successType: DELETE_ACCOUNT_COMPLETE,
    failType: ERROR
  },
  process({ getState, action }) {
    const id = action.payload;
    return deleteAccount(id)
      .then(() => id); // return id to be use for payload
  }
});

Obviously there are many ways to do this. You could even call your updateAccount from your deleteAccountLogic but I think it is always nice to have this completely isolated into an API. Testing is easy and then it can be used in a bunch of different ways in or outside of redux. The redux-logic code ends up being really slim, just mapping what api to call and what action to dispatch and accessing action payload values.

Hopefully that helps.

from redux-logic.

jeffbski avatar jeffbski commented on August 11, 2024 2

@tylerlong Thanks for sharing your thoughts.

Yes with redux-logic I give you the freedom to use the JS style you and your team are most comfortable with.

So you can use:

  • callbacks
  • promises
  • async/await
  • observables

Each has its own strengths, but mostly it comes down to what you are comfortable working with for your team. Many of the other solutions force you into using things like generators or observables, but I want to give you the freedom to choose what's best for you since those skills take time to become proficient at.

Anyway, the process hook has a variable signature, so you can call it with or without dispatch and done. I discuss them a little more here.

In a nutshell if you simply omit the dispatch and done from your function then redux-logic uses the return value for dispatching. If it is undefined it dispatches nothing. If it is an object then it will dispatch that. If you return a promise then it dispatches whatever the promise resolves/rejects with. And if you return an observable then it subscribes to that and dispatches whatever it emits.

redux-logic takes the return value, figures out which of those applies and then appropriately dispatches for you.

So this makes it easy to adapt to whatever style you prefer.

Promises and/or async/await are a really great way to go if you are comfortable with them. The resulting code is nice and clean and easy to test.

You can also set processOptions to automatically apply actions or action creators to your resulting data or error. So instead of having to call the action creator as part of your code, you can just specify a successType and/or failType action or action creator which will be given your result/error and converted to an action used for dispatching. Using these is entirely optional but it even further cleans up the code. I discuss this in the readme here

So to answer your question. You don't have to call dispatch anywhere if you omit the dispatch from your function call (or had set the processOptions.dispatchReturn = true). redux-logic will use what you return value to figure out what to dispatch and if it was a promise or observable it will wait for it to resolve then it will dispatch the result(s).

from redux-logic.

jeffbski avatar jeffbski commented on August 11, 2024 2

@tylerlong By default, when you use auto-dispatching either by omitting the dispatch in the signature or setting the processOptions.dispatchReturn = true, then it will dispatch whatever your promise resolves. So you can resolve with an action object that is shaped however you want.

const logic = createLogic({
  type: FOO,
  process({ getState, action }) {
    return axios.get(url)
      .then(resp => resp.data.items) // use items property
      .then(items => ({ type: FOO_SUCCESS, myitems: items })); 
    }
});

So I am resolving with an action obj that puts the items in property myitems. You are in full control of the action object shape dispatched by what you resolve with.

You could also use an action creator to create the obj like this

// action creator for fooSuccess
const fooSuccess = items => ({ type: FOO_SUCCESS, myitems: items });

const logic = createLogic({
  type: FOO,
  process({ getState, action }) {
    return axios.get(url)
      .then(resp => resp.data.items) // use items property
      .then(items => fooSuccess(items)); 
    }
});

So that is what happens by default, you create the action obj or use action creator to create action obj and then resolve that from the promise and it will be dispatched.

If you add the processOptions.successType then you instead of just resolve with the data and redux-logic will wrap it in an action object. If successType is just a string, then it assumes it is the action type and it will use the flux standard actions FSA format which uses payload as the property to put your data. If you would rather have more control and use a different property like above, then you can provide an action creator to successType and it will call that to create the action object.

So here is the previous example but using processOptions.successType = fooSuccess

// action creator for fooSuccess
const fooSuccess = items => ({ type: FOO_SUCCESS, myitems: items });

const logic = createLogic({
  type: FOO,
  processOptions: {
    successType: fooSuccess // use this action creator to create success action object
  },
  process({ getState, action }) {
    return axios.get(url)
      .then(resp => resp.data.items); // use items property for resolve    
    }
});

This will dispatch with { type: FOO_SUCCESS, myitems: items } since it resolves with items and then that is passed to the fooSuccess action creator to return the action object.

And similar for the processOptions.failType. If the failType property is a string then it follows FSA format for errors otherwise you can pass an action creator to failType and have full control of how you want it to look.

I discuss successType and failType options more here

So you are free to just resolve with the action object or use successType and failType to help you create them. Either way is completely fine.

The reason successType and failType exist it just to make a little more of the code declarable but it is up to you whether to use those features or not.

You can read about the motivation for flux standard actions FSA here. Many people like to use that convention but it is entirely up to you. FSA format explains the format for errors as well as success.

Yes, you are correct about the default format for errors (FSA format) if you had only given failType a string action type rather than an action creator. It sets payload as the error and adds an additional error flag set to true.

from redux-logic.

jeffbski avatar jeffbski commented on August 11, 2024

That's an interesting question. I suppose if there was interest we could provide other adapters/middleware that would allow it to work with other flux/messaging/event bus type libraries (anything that deals with action-style messages that dispatch and have a way to observe) or even just using observables.

I'll have to ponder that a bit. It seems possible though since the actual redux API is so minimal. Whatever we would be using would need some hook for intercepting (like middleware), and a dispatch mechanism.

However you are correct in that it is pretty easy to keep the real meat of the logic as separate calls, so you just wrap those core calls with redux-logic to hook them in and thus they are reusable for other technology as well.

I really like returning promises or observables to my redux-logic (and even setting actions with the processOptions), that keeps my logic really thin and easy to test and by keeping those core API calls as functions in a library they are independently usable in anyway I want to use or combine them.

Here is an example. If I create a library MyAPI which does all my I/O calls and returns either promise or observable then I can simply pass in whatever I need out of the action and state and just return the promise or observable which resolves to the data. Then since I have processOptions set they will wrap the data into an action (on success or failure). Thus I keep my API pure and clean (it knows nothing about actions or dispatching), then redux-logic is the adapter/bridge which brings it into the redux world (and that code is mostly declarative, pretty thin).

const fetchPollsLogic = createLogic({

  // declarative built-in functionality wraps your code
  type: FETCH_POLLS, // only apply this logic to this type
  cancelType: CANCEL_FETCH_POLLS, // cancel on this type
  latest: true, // only take latest

  processOptions: {
    successType: FETCH_POLLS_SUCCESS, // dispatch this success act type
    failType: FETCH_POLLS_FAILED, // dispatch this failed action type
  },

  // Omitting dispatch from the signature below makes the default for
  // dispatchReturn true allowing you to simply return obj, promise, obs
  // not needing to use dispatch directly
  process({ getState, action }) {
    const { cat, dog } = getState();
    const id = action.payload;
    return MyAPI.getFoo(id, cat, dog); // return obj, promise, or observable
  }
});

So depending on what other type of environment you might want to use redux-logic in, it seems like it might be possible to create a middleware/adapter for it.

Thank you for the encouraging words. I really like using redux-logic personally so I am glad that others also feel the same way. I appreciate suggestions and help getting the word out about it since there are so many libraries these days, people don't know which ones are worth checking out, so word of mouth and blog articles are so valuable. I plan on writing tutorials, creating a recipe guide, and doing some videos very soon.

from redux-logic.

tylerlong avatar tylerlong commented on August 11, 2024

You have done a good job. Recently I gave an technical sharing in my team and I went through all of the alternatives and finally ended with redux-logic: https://github.com/tylerlong/tech-sharing-redux-async

I do think at the moment it's probably the best choice.

In your example above, you don't call dispatch and return obj/promise/observable instead, then where is the final result dispatched? I mean you need to call dispatch somewhere in your app. I am not an Redux expert please give me some hints for the solution. Thank you very much!

from redux-logic.

jktravis avatar jktravis commented on August 11, 2024

I have a similar question to the above, but rather than using a logic in other places, I'd like to simply reuse it in place by dispatching actions. I have an action that involves a hierarchical calculation that I want to reuse for other actions. I know that I can dispatch multiple actions by calling done() at the end; however, in one scenario, I need the previous action to complete before continuing, while in another scenario, it doesn't matter.

Example:
UPDATE_ACCOUNT: Updates the value of the given account, and updates all the parent's values based on the value provided all the way up the tree.

DELETE_ACCOUNT: Zero's out the provided account, dispatches UPDATE ACCOUNT, then deletes its current children and then itself.

In the DELETE_ACCOUNT scenario, my calculation requires that the account still be available in state to the UPDATE_ACCOUNT action before it tries to delete itself. I see the use of returning a promise in your example above, but I'm just not sure how that works exactly.

Would you mind providing a recommendation to my scenario? Thanks!

from redux-logic.

jktravis avatar jktravis commented on August 11, 2024

Fantastic. Thanks, @jeffbski. This makes a lot of sense. I guess I had overlooked the whole auto-dispatch-on-return feature.

from redux-logic.

jeffbski avatar jeffbski commented on August 11, 2024

Yeah, you can do the same thing using dispatch if you prefer, but the auto-dispatching return is pretty nice IMO.

from redux-logic.

tylerlong avatar tylerlong commented on August 11, 2024

One minor question about auto-dispatching:

So if promise resolve was data, what it dispatches will be { type: 'XXX_YYY', payload: data }. What if I don't like the name payload?

And for the error dispatching, are there documentation for its format? From your example above I guess it is { type: ERROR, payload: err, error: true }.

from redux-logic.

jeffbski avatar jeffbski commented on August 11, 2024

I believe we have discussed everything so I will close this, we can reopen if necessary

from redux-logic.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.