-
-
Notifications
You must be signed in to change notification settings - Fork 192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Action/Thunks listeners #247
Comments
This comment has been minimized.
This comment has been minimized.
Hey @ctrlplusb, I really like this proposal. I agree with the direction your going in and also think this would be easier to grok than the
Overall, I think your suggested changes are an improvement to the library. Thanks for asking for our feedback. |
RE naming: I think However changing "listener" to "listenTo" makes it clearer:
OR lose the word 'listen' in favor of something more natural-language, like:
I like 'actionOn' / 'thunkOn'. It's concise and clear because "on" is a universal action-prefix - onChange, onClick, onEvent, etc. If that isn't clear enough then a more verbose option is:
|
There is still a gap in the listener params. There are actually 2 "payloads" it needs... The first is the "action payload" - the input to original action. This is the only payload for an "action listener", and may contain important identifiers/metadata for a "thunk listener" as well. The second is the "response payload" - the data returned to a thunk, like an API response. Since an error IS a "response", I'd follow the FSA concept of error-as-payload, with a boolean One way to provide both payloads is to add a "response" key for the response-payload. The "payload" key is the original payload. So both action and thunk listeners have Putting this all together... // Action payload
target = {
name: "...",
type: "...",
payload: [ original ]
}
// Successful thunk response
target = {
name: "...",
type: "...",
payload: [ original ],
response: { data-object }
}
// Failed thunk response
target = {
name: "...",
type: "...",
payload: [ original ],
response: { error-object },
error: true
} |
The "succeededType" name bugs me. ;) It's hard to read and spell! Normally I refer to "success" and "fail" handlers; never a "succeeded handler". So how about these more succinct names...
If you want to keep them matching the string versions, then just change the "success" type, including the new string:
|
It would be convenient to also have a "thunk.settledType" for brevity, but it's not important. Could add it at any time. |
Amazing feedback both - really appreciate you taking the time to do so. Naming I agree with @allpro in that I like some of your suggestions @allpro, my favourites being:
What do you think @jamaljsr ? UI Loading states I appreciate you raising this as a common concern @jamaljsr - whilst it may be useful to expose some additional meta data I would prefer we leave this level of a problem in user land for now. Like you state, a helper could fairly easily be written to encapsulate this behaviour. One issue I can see though - the thunk/action type properties are on available on the action/thunk creators (i.e. the functions used to dispatch), not on the model definitions. Therefore your following code would not work: saveListener: listenerAction(
(state, target) => {
// you are referencing the model definition, not the resolved action creator
// 👇
const { startedType, succeededType, failedType } = storeModel.saveTodo; There is an issue here for us. I can't predefine these types on an action/thunk definition as I won't know it's fully qualified path until it is composed into a full store model and then passed into I have been trying to think of a mechanism around this that isn't overly complex. One option I could think of is returning the resolved target types as a 3rd parameter to actions/thunks. saveListener: listenerAction(
// Resolved target types as an array with matching array index
// 👇
(state, target, { targets }) => {
const [startedType, succeededType, failedType] = targets;
switch (target.type) {
case startedType:
state.status.loading = true;
break;
case succeededType:
state.status.error = target.error;
// intentional "break;" missing to continue below
case failedType:
state.status.loading = false;
break;
}
},
// 👇 the resolved types of targets will be provided to the handler
actions => [
actions.saveTodo.startedType,
actions.saveTodo.succeededType,
actions.saveTodo.failedType,
]
) The same could apply to a saveListener: listenerThunk(
// Resolved target types as an array with matching array index
// 👇
(actions, target, { targets }) => {
const [startedType, succeededType, failedType] = targets;
// ...
},
// 👇 the resolved types of targets will be provided to the handler
actions => [
actions.saveTodo.startedType,
actions.saveTodo.succeededType,
actions.saveTodo.failedType,
]
) Thunk Stage Type Names @allpro I like your concise version. I have mistyped succeeded many times already. Happy to go with the following then.
Thunk Results "Payload" @allpro another great point. You are on fire. Yep, I can understand how this might be useful. I am happy to consider extending the const {
name, // Short type name
type, // Fully qualified type
payload, // Actual payload provided to target
result, // ONLY used when listening to thunks. Will contain anything returned by a thunk.
error, // ONLY used when listening to thunks. Will contain the error if they failed.
} = target; I don't really see the need to wrap the payload in an array, or equally to wrap the response. I personally prefer the more explicit |
This comment has been minimized.
This comment has been minimized.
Great stuff! I just have a quick question that might have been discussed before. What is the reasoning behind the order of the arguments for |
Thanks @ludwigbacklund - I really like the suggestion of putting the target resolvers first in the arguments. That reads much nicer. 👍👍 Yeah they can't exist without a resolver so this makes great sense. |
const {
name, // Short type name
type, // Fully qualified type
payload, // Actual payload provided to target
result, // ONLY used when listening to thunks. Will contain anything returned by a thunk.
error, // ONLY used when listening to thunks. Will contain the error if they failed.
} = target;
For clarity, I was not suggesting to "wrapping the payload in an array". I used square bracket notation to mock the payload value because it can be anything, like a simple ID. I didn't want to imply it had to be an 'object'. If you are going to put errors in an error key, it would be good to document that a 'target' can have ONLY a "response" OR an "error" key - never both. This is because the error is the response! This may help clarify things since this differs from regular action syntax. To clarify the either/or nature, here's a common async pattern. function fetchSomething(payload) {
return api.getSomething(payload);
}
thunk(async getSomething() {
let resp = null
try {
resp = await fetchSomething({ id: 1234 })
}
catch(error) {
resp = error
}
}) Note that |
I updated feature-request #241 because it would impact the listener // Action params
action(state, payload, meta)
thunk(actions, payload, meta)
// Dispatch action - FSA format
dispatch({
type: '...', // Required
payload: { ... }, // If exists
meta: { ... } // If exists
})
// Sample thunk listener, with all possible params
actionListenTo(state, target - {
type: '...',
name: '...',
payload: { ... },
meta: { ... },
response: { ... },
error: ?
}) |
Naming UI Loading states const auditModel = {
logs: [],
onCriticalAction: actionOn(
// targetTypes contained within 3rd argument (to thunk or actions listeners)
// 👇
(state, target) => {
// Our targetTypes matches the array index of our targetResolver function
// 👇 0 👇 1 👇 2
const [loggedIn, loggedOut, addedTodo] = target.resolvers;
switch (target.type) {
case loggedIn: \\...
case loggedOut: \\...
case addedTodo: \\...
}
},
(actions, storeActions) => [
storeActions.session.loggedIn, // 👈 0
storeActions.session.loggedOut, // 👈 1
storeActions.todos.addedTodo, // 👈 2
]
)
} Everything else looks great! Thanks for taking our input 👍 |
I agree with @jamaljsr that a third param seems unnecessary since you already have a catch-all target object. The key "resolvers" seems logical, but here are some other ideas for variety:
|
Wow, this has been so great. I am truly grateful for all this super valuable feedback and suggestions everyone. It feels good to be making these decisions more community driven. ❤️ Here is the updated proposal based on all your feedback. Proposal v3This proposal introduces two new listener APIs, namely; In addition to these new APIs the semantics of the actions that fire to represent each stage of a thunk have changed. These changes are aimed at satisfying the common use-case, whilst making them more convenient. This proposal deprecates the We will briefly introduce the two new listener APIs, with an example of each. Thereafter we will review the concepts and configuration that are shared between them. APIsThe
actionOnA listener action is responsible for performing updates to state in response to configured targets being executed. onAddTodo: actionOn(
// targetResolver:
actions => actions.addTodo,
// handler:
(state, target) => {
state.auditLog.push(`Added a todo: ${target.payload}`);
}
) thunkOnA listener thunk is responsible for performing side effects (e.g. a call to an HTTP endpoint) in response to configured targets being executed. onAddTodo: thunkOn(
// targetResolver:
actions => actions.addTodo,
// handler:
async (actions, target) => {
await auditService.add(`Added a todo: ${target.payload}`);
}
)
|
Looks solid to me! 👍 A couple typos I see:
|
Awesome, thanks @jamaljsr - I've fixed those typos too. 👍 |
Ok, I've built out this RFC, complete with tests and Typescript definition updates. I have yet to update the website documentation, but will do so. I am going to get this merged into the We'll leave |
Once again thank you all. And please feel free to continue to criticise v3. I'd like us to get to a truly happy place with the API and then lock it down for long term support, avoiding breaking changes at all costs. |
* Initial refactor and introduction of new listeners apis * Initial typescript definition refactor for new listeners * Updates the action creator types * Renames listener APIs * Updates listener implementation per community feedback * Exposes action listeners on actions types
🎉 Great work Sean. It's damn hard to fit so many features into a simple, intuitive API. I hope to soon start training 25 developers, in 3 countries, to use Easy-Peasy, and phase-out Saga. So it's great news that the API is stabilizing. |
Awesome, thats great to hear @allpro. I am really appreciative of your constructive criticisms and recommendations that allowed us to evolve and simply the API even further. 👍 The website docs, including the tutorials are busy getting massive overhauls on them. They should be a lot more useful as a tool in teaching Easy Peasy as they will follow a structure where you are encouraged to refactor an existing application as you learn each of the APIs. I'm hoping they may form a platform with which to base a video tutorial series on too. 🤞 |
Fully documented within v3 website now |
* Initial refactor and introduction of new listeners apis * Initial typescript definition refactor for new listeners * Updates the action creator types * Renames listener APIs * Updates listener implementation per community feedback * Exposes action listeners on actions types
* Initial refactor and introduction of new listeners apis * Initial typescript definition refactor for new listeners * Updates the action creator types * Renames listener APIs * Updates listener implementation per community feedback * Exposes action listeners on actions types
* Initial refactor and introduction of new listeners apis * Initial typescript definition refactor for new listeners * Updates the action creator types * Renames listener APIs * Updates listener implementation per community feedback * Exposes action listeners on actions types
Hey all;
The previous RFC process I opened for the
computed
property was extremely useful - I received a lot of quality feedback which helped guide me to the best API. Therefore I'd like to use the same process for an adjusted action/thunk listener API.The current action/thunk listeners API (configured via the
listenTo
config) has a few shortcomings, namely:A lot of these concerns have been raised very helpfully by @allpro - which is highly appreciated.
Prior to us going to v3 (with an opportunity to introduce some small breaking changes), I definitely would like us to address these shortcomings. Either enhancing the existing API, or providing an alternative richer one.
Related #233 #244 #237
I am going to be laying out a few proposals based on some experiments I have made, taking into account the above related issues.
The text was updated successfully, but these errors were encountered: