-
-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Added getContext/setContext effects, where context is acting like dyn… #735
Conversation
also I think like some way for removing the context should be available as for now it wouldnt be possible to 'uncover' higher scope function* saga() {
yield setContext({ a: 1 })
yield fork({
yield setContext({ a: 2 })
yield setContext({ a: undefined })
yield getContext('a') // undefined, no way to reach { a: 1 } at this point
})
} hating to put 3 effects for this rather simple feature - |
First of all, thanks for spending time on this. Some thoughts:
function* a() {
yield setContext({foo: 1});
yield fork(function* b() {
yield setContext({foo: 2});
yield fork(function* c() {
// ..
});
});
} Should |
src/internal/proc.js
Outdated
// TODO: if object returned -> return shallow copy? | ||
// should yield getContext() be available to access context? | ||
// if yes then shallow copying it is a must to avoid unexpected mutations | ||
cb(taskContext[prop]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Context property could be anything, not just plain object, so we can't safely copy it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I think we should throw if property doesn't exist in context to distinguish between the case where context doesn't have a property and the case where context has property with value undefined
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
eh, you static typing guys and your habits! 😄
Context property could be anything, not just plain object, so we can't safely copy it.
ofc it would be checked beforehand if it is a plain object to see if we should make a shallow copy
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I tend to agree with you that throwing is not really necessary here as any relevant checks could be easily performed on the user side.
I dont think it will be a problem as this context feature is less convenient to use, so people will always prefer accessing variables than this within task which uses
I think that could lead to unexpected bugs and also updating the
Nah, didnt think about uncovering like this, just 1 step into the upper |
Another way to avoid
Is this really a library responsibility? We don't shallow copy |
Good point, however I feel like |
Here's a counterexample: const obj = {
foo: null,
setFoo(foo) {this.foo = foo},
getFoo() {return this.foo}
};
yield setContext({obj});
// ...
const obj1 = yield getContext('obj');
yield call([obj1, obj1.setFoo], 1);
// ...
const obj2 = yield getContext('obj');
const foo = yield call([obj2, obj2.getFoo]); // null Here, the value of |
Hm, interesting. Its really a scary situation and compelling one at the same time to drop shallow copying. Wondering only which usage would be more often and therefore which approach would give us less footguns in the end. In theory we could make 2 versions of this effect getContext and getContext.impure In general im in favor of immutability and this-less programming, aint saying though that everyone should. This feature would be experimental anyway and rather an escape hatch which should noy be used in trivial cases anyway, so... i dont know which is better now as a default |
I absolutely agree with you, but my reasoning is this: for pure things we already have Redux store and actions which we can access in sagas at any level using appropriate effects. We would use Context to access non-pure third-party library objects. Another use case is to share Channels between sagas, however I guess shallow-copying a Channel would be safe. |
src/internal/proc.js
Outdated
subscribe, | ||
dispatch, | ||
getState, | ||
(detached ? null : taskContext), // TODO: topContext should be passed in case of spawn? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why cant we just pass the parent context?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reasoning behind it - spawn
creates a detached
task, which is acting like a top task, so would it be good to propagate taskContext
(parent context) to it? behaviour would differ from other propagations (error/cancellation) in terms of detached (spawn
) and non-detached tasks (fork
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah but you could also view spawn
as a simple child task with different behavior regarding cancellation/errors? And also with analogy to lexical scope, it gets its params from its parent task, so could be the same from dynamic scope (context)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could be ofc, but what i have proposed felt more natural when i was implementing this, however that was just a question for reviewers (so mostly you :P) left in the source code
also a little bit connected is this - #763 (comment)
in regards of spawn/fork debate, cancel propagation and maybe even need to distinguish rootTask from regular tasks for code-splitting purposes and proper cancelation logic on route changes, would appreciate your input about those matters there greatly!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree with @yelouafi, if we disable propagating context on spawn
, it would make context
a much less convenient mechanism for dependency injection.
That said, I think that runSaga
should behave just like you said — starting with an empty context by default. Another reason is that otherwise we'd have to somehow link runSaga
with sagaMiddleware
instance.
src/internal/io.js
Outdated
} | ||
|
||
export function setContext(props) { | ||
// TODO: check if props is object/plain object? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, I think we should restrict props
to be a plain object.
@@ -91,5 +93,14 @@ export default function sagaMiddlewareFactory(options = {}) { | |||
return task | |||
} | |||
|
|||
sagaMiddleware.setContext = (props) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the purpose of this API? I can see cases where you need to change the root context as a result of some event, but I think it's more convenient to do this in the root saga.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just found out this was proposed by myself: #713 (comment)
Although I think only one of options.context
or sagaMiddleware.setContext()
is enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well, one is needed upon creation and latter might be used in a runtime, could be ofc in response for some action but for more complex contexts it would require action to contain ur dependency which could be not serializable fully, which i dont like - if you for example need to pass some event bus with methods on it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. 👍
src/internal/middleware.js
Outdated
// TODO: or maybe `setContext` should be available on task and this one should act only as proxy method? | ||
// would that be useful? | ||
// const task = yield takeEvery(...) | ||
// const task.setContext({...}) or maybe even - yield setContext(task, {...})? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO the context of a task should be a private thing, just like state
in React components. The component itself is an owner of its state and it shouldn't be accessible from its parent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actually as React's setState
is available on the instance of the component - it could be used outside and therefore its not so private
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, also I can see some parallel with sagaMiddleware.setContext
. Maybe then we just leave task.setContext
and remove sagaMiddleware.setContext
, as the latter can be done with sagaMiddleware.run(...).setContext
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hm... at first I thought that this is a valid point and should be done, but after a while i think in the world of code-splitting its nice to have a possibility to share this top-level middleware context among root sagas, as there could be many of those, and when puting this only on task itself it would require such verbose code in order to share it 'globally':
const context = {...}
sagaMiddleware.run(...).setContext(context)
// ... later
sagaMiddleware.run(...).setContext(context)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, I've overlooked the case when sagaMiddleware.run
is called multiple times.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the only thing im not so happy about is that sagaMiddleware
is kinda superior to the runSaga
here, as the latter do not have possibility to share such things in an elegant way, but I guess one can always write some wrapper function up to his needs
src/internal/proc.js
Outdated
} | ||
|
||
function runSetContextEffect(props, cb) { | ||
Object.assign(taskContext, props) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A check for props
being a plain object should be added here as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
such checks are usually performed in effect creators, it would only double the same check if we put it here also, as its not possible to reach this - runSetContextEffect
in any other way that yield setContext(...)
src/internal/io.js
Outdated
@@ -153,6 +155,16 @@ export function flush(channel) { | |||
return effect(FLUSH, channel) | |||
} | |||
|
|||
export function getContext(prop) { | |||
check(prop, is.string, `getContext(prop): argument ${ prop } is not a string`) | |||
return effect(GET_CONTEXT, prop) // TODO: maybe accept functions also? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc @aikoven
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we do Object.assign
and not deep merge, I'd view context
as a flat object. Thus not sure if complex accessors are needed. Maybe I'm missing some use cases, but I'd rather add such features on demand.
@aikoven EDIT:// heh, while writing this you have already replied 👍 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we have a consensus finally 😉
I think the code is final now, so you can have last look on it - whats missing before merging is some tests + docs, ill get them done in this week and merge whole thing to the master. |
src/internal/middleware.js
Outdated
import proc from './proc' | ||
import { emitter } from './channel' | ||
import { ident } from './utils' | ||
|
||
export default function sagaMiddlewareFactory(options = {}) { | ||
export default function sagaMiddlewareFactory({ context = {}, ...options }) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Won't this break if we call factory without arguments?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it would, thanks for noticing! gonna fix it with default param in a minute
Guys, do you think this branch is going to get merged eventually? |
@AriaMinaei yes, it sure will! I just need to get some time to merge it and some other branches, so I can release new version, sorry about the delay |
Is there anything I can do to help out with this PR? |
@neurosnap, i think its basically done. Will review it again in the weekend and gonna publish |
…amic scoping between parent/child tasks (resolves #713)
Released, not docs yet - but if you know about the feature and want to use it - go ahead. Let me know if everything works as expected. |
…amic scoping between parent/child tasks (resolves #713)
Just a quick draft of this proposed API. I've put several
TODO:
s in the code, they act as thought-placeholders and I would appreciate commenting each one of them.Also there I've spotted a difference between standard
middleware
API andrunSaga
.middleware
- options are passed once, so encapsulated by the factory) and each subsequentmiddleware.run(saga)
will receive them (alsocontext
now)runSaga
- each call expects theoptions
all over again, so subsequent calls do not sharecontext
and it cannot be shared among 'top sagas' (at least not without creating atopContext
withinrunSaga
module)cc @aikoven