Open zeroooooooo opened 6 years ago
Can you post the code that you used to register the actual action via .addReducer()
? Since you have some kind of custom ${namespace}/
prefixing, maybe the string is not the expected one?
You could also check with the Redux Devtools which actions was dispatched when you .next() on the observable directly. Do the strings match?
Note: I added the string-based action dispatched only for some special cases, like using the devtools, replaying/serializing actions between frames or over the network etc. I highly advice to use observable-based action dispatch, except for the cases where this is not possible.
Hi, @Dynalon, sorry for the late response.
I post a simplified demo here.
The code I used to register action is the same as the example code as follows: connect.tsx dogs.tsx
import { Store } from 'reactive-state';
import { ActionMap, connect } from 'reactive-state/react';
import { defer, Subject } from 'rxjs';
import { filter, map, switchMap } from 'rxjs/operators';
import { Dogs } from './dogs';
interface Dogsslice {
breedNames: string[];
selectedBreed?: string;
breedSampleImageUrl?: string;
}
// Use an observable as action to fetch a list of all available dog breeds
const fetchBreedNames = defer(() => fetch('https://dog.ceo/api/breeds/list')).pipe(
switchMap(response => response.json()),
map(body => body.message as string[])
);
// use a Subject that will trigger fetching an example image by a given breed name
const getSampleImage = new Subject<string>();
// the actual logic that will fetch the image when the action is dispatched
const fetchSampleImage = getSampleImage.pipe(
switchMap(breedName => {
return fetch(`https://dog.ceo/api/breed/${breedName}/images/random`);
}),
switchMap(response => response.json()),
map(body => body.message as string)
);
export default connect(Dogs, (store: Store<Dogsslice & {[property: string]: any}>) => {
const branchName = 'branch:dogs';
const slice = store.createSlice(branchName, { breedNames: [] });
// add reducers/action pairs - note that the string names are only for debugging purposes in devtools and
// not required
slice.addReducer(fetchBreedNames, (state, breedNames) => ({ ...state, breedNames }), `${branchName}/FETCH_BREED_NAMES`);
slice.addReducer(fetchSampleImage, (state, imageUrl) => ({ ...state, breedSampleImageUrl: imageUrl }), `${branchName}/FETCH_SAMPLE_IMAGE`);
slice.addReducer(getSampleImage, (state, breedName) => ({ ...state, selectedBreed: breedName }), `${branchName}/SELECT_BREED_NAME`);
const props = slice.watch().pipe(
filter(state => state.breedNames.length > 0),
map(state => {
return {
breedNames: state.breedNames,
selectedBreed: state.selectedBreed,
breedSampleImageUrl: state.breedSampleImageUrl
};
})
);
const dispatchProxy = async (payload: any, namespace: string) => {
const actionName = namespace ? `${namespace}/${payload.type}` : `${branchName}/${payload.type}`;
slice.dispatch(actionName, payload.data);
// getSampleImage.next(payload.data);
};
const actionMap: ActionMap<Dogs> = {
onGetNewSampleImage: getSampleImage,
dispatchProxy
};
// the store we got as 1st argument is a clone and automatically destroyed when the
// connected component gets unmounted. Upon destroy, all action/reducer pairs that we added inside
// this function become inactive. This also applies to all child/slice stores created from the clone -
// they will be destroyed, too, upon umount. To show the auto-destroy feature, we log to the console:
slice.destroyed.subscribe(() => console.info('Dog slice got destroyed, all action/reducer pairs on this slice were removed'));
return {
actionMap,
props
};
});
the diff is I add a function to proxy the dispatch as follows:
const dispatchProxy = async (payload: any, namespace: string) => {
const actionName = namespace ? `${namespace}/${payload.type}` : `${branchName}/${payload.type}`;
slice.dispatch(actionName, payload.data);
// getSampleImage.next(payload.data);
};
const actionMap: ActionMap<Dogs> = {
onGetNewSampleImage: getSampleImage,
dispatchProxy
};
this function accept the action from the component and help to proxy the action by type,
private test = () => {
const t = ['affenpinscher', 'african', 'airedale'];
this.props.dispatchProxy({
type: 'FETCH_SAMPLE_IMAGE',
data: t[~~(Math.random() * 10 % 3)]
});
// this.props.onGetNewSampleImage(t[~~(Math.random() * 10 % 3)]);
}
but the result is if I call the onGetNewSampleImage it will trigger 'SELECT_BREED_NAME' and 'FETCH_SAMPLE_IMAGE', if I call the dispatch function it will trigger reducer directly. the async action didn't been call.
I saw the annotation in the source code, and know it is not adviced to used manully dispatch extensively.but I want to do something as follows:
so I think dispatch manully is a necessary feature.
I don't know if I explained it clearly. If you need more specific details, I can try to explain the application's organization and the reason.
Thanks.
First of all: There is nothing wrong with your dispatch logic. Upon clicking the button, the FETCH_SAMPLE_IMAGE
action is correcty dispatched, and you can even see a state change that puts the dog's race into the state. See the redux devtools:
The issue is that you are dispatching a dog's race name instead of an image url - the FETCH_SAMPLE_IMAGE
action requires a URL as a payload.
Now, when you dispatch through an observable, your are calling .next('african')
on getSampleImage
subject - this is not a string-based action! It is just a Subject that is used in an Observable pipeline:
// This is a Subject that is NOT an action - this is used in the next line on fetchSampleImage to
// build a pipeline!
const getSampleImage = new Subject<string>();
// the actual logic that will fetch the image when getSampleImage is .next()'ed
const fetchSampleImage = getSampleImage.pipe(
switchMap(breedName => {
return fetch(`https://dog.ceo/api/breed/${breedName}/images/random`);
}),
switchMap(response => response.json()),
map(body => body.message as string)
);
Here you can see, getSampleImage
is piped to custom logic which maps the dog's race to a url, fetches a JSON response, and gets a new url that points to an actual image of the dog.
In your case I see several options that you have:
export
the getSampleImage
Subject and import {}
it elsewhere. This is basically my suggestion not to use string-based action dispatches at all, but rather reference the Observable. You could also put all your actions that should be called from other modules into a single file and import from there.
If you still want to stay with string-based action dispatch (this can be wise to decouple modules!), you could add a "reducer" with a side-effect and a string name so you can dispatch it:
slice.addReducer('branch:dogs/GET_SAMPLE_IMAGE', (state, race: string) => {
// this is a side-effect
getSampleImage.next(race);
// return an unaltered state.
return state;
});
And dispatch GET_SAMPLE_IMAGE
now. This is somewhat ugly, as reducers are supposed to be side-effect free. I am thinking of adding a .addSideEffect()
function in future releases of reactive-state
to cover this. I create a PR for this change: https://github.com/zeroooooooo/helloworld/pull/1
You could also use a global pubsub messenger (maybe use EventEmitter
for the browser) and publish actions over this global message bus. In your modules, you would subscribe to the string-based events on the EventEmitter, and call .next(payload) based on the pubsub payload. You wouldn't be using string-based actions of reactive-state
at all, but rather use the string-based pubsub only. This not only would decouple your application using strings, it would also decouple your modules from reactive-state
as they would only use the EventEmitter. This is a very fancy architectural pattern, that I think you should only consider for large-scale applications.
I'd recommend option (1) - do not use string-based action dispatch. But if you absoluteley need this, go with option (2).
Thanks a lot for the replying and the suggestions. @Dynalon
I thought dispatch would trigger the observable and through the pipeline, I got it wrong...
addSideEffect should be a good choice.
export and import the actions is a useful method to solve the problem. but with the application growing, there will be much more modules, and the relations between the slice may be complex. and many boilerplate code must be written.
It's a common problem to the application I've developed, so I try to find a common pattern for the most application and in different frameworks. and it seems make a break through by using vue with vuex recently. when I met this state management tool, I think this may solve the problem in different frameworks. great work!
what I want to do is something as follows:
import { Service } from './service/base';
export default {
name: 'Coke Factory',
shareState: {
cokeCans: {
type: 'string',
default: ''
},
cokeWater: {
type: 'string',
default: ''
},
coke: {
type: 'string',
default: '',
action: Service.productCoke
}
},
componentsList: [
{
name: 'Hello',
globalState: [
'root/userInfo',
'trunk:counter/counter',
'branch:other/iron'
]
},
{
name: 'World'
}
]
};
Think of the pages are as containers, they are make up of the components from the configuration, and we have a HOC which can automatically created the actions and the reducers by the describe from the configuration(shareState key is the content, it can tell which state should be share in the page), and when dynamic load the related page, it will register the whole thing. and pass the state as props to the page's components. and when component wants to change the share state, it needs to dispatch an action(by type and payload data), and the page will proxy the action, and then state will be changed, then components which subscribe the state will be reRender. So I think the string-based dispatch is better for this situation.
page = HOC(Mixins in vue) + configuration
I think it's a good solution for me. It has some advantages as follows
This is most of my thoughts.
I think this state management is a great direction. thanks again!
Thanks for your detailed feedback, always good to hear what problems other developers face and how they come up with solutions. That helps making reactive-state
better in the future!
Let me give you some insights about the issues I see with string-based action dispatch (which I disliked in Redux and was the reason to come up with reactive-state
in the first place!):
dispatchProxy
completely destroy type safety. I can dispatch a string
to the FETCH_SAMPLE_IMAGE
action, or a number, an Object or whatnot. The compiler will not report an error. If you have a large application and you refactor the type of the action payload, you must remember to change the type wherever you dispatch that action. When you use the Subject
, the compiler will always complain when you try to dispatch the wrong type. If you refactor the type of Subject<string>
to Subject<Url>
, the compiler will ultimately tell you all the lines of code you need to change, as it knows where you called .next()
with the wrong type on that subject..addReducer()
, there might be areas in the code still dispatching the action. Again, the compiler will not complain. If you were using a Subject
, you would most likely remove the Subject along with the addReducer()
and the compiler will error on all places that still try to dispatch an action to trigger this reducer. (This is especially true if you use the noUnusedLocals
typescript compiler flag, as it will tell you that the unused Subject needs removal)Subject
actions that are module internal. That is, you will not export
them beyond module boundaries - that is you will not make them part of the API of the module that is meant to be consumed by other entities. You will only export
those Subjects that are meant to be dispatched by other modules. With the string based approach, you completely lose this separation. Now, you came up with a {namespace}/{branch}:{action}
approach to kind of solve this. But the import/export
and the structure of modules in JavaScript/TypeScript is intended to be used for this encapsulation. Your custom string-prefix approach is a newly introduced semantic, that I see as a kind of unnecessary abstraction.export and import the actions is a useful method to solve the problem. but with the application growing, there will be much more modules, and the relations between the slice may be complex. and many boilerplate code must be written.
I don't see any boilerplate. The boilerplate will be magic string constant that exist throughout your application. You can put all Subject
s that trigger registered action/reducer in a root-level file, or your modules entry point. All you need to do is to import {}
it and call .next() on it. Benefit: You will see by the import path which module requires what other module. So your inter-module dependencies are clearly expressed. You cannot remove Module A and have Module B still present, if B dispatches an action to A - this will result in a compiler error. If you want optional modules - that is module B might dispatch to A if its available at runtime, you can use a dynamic import()
statement: const action = import('othermodule/A').someAction
statement to test for the module (and use try/catch or .then/.catch on a promise to handle presence/absence of the module). Again, 100% compiler typesafety.
To sum up the benefit of strictly using Observables/Subjects for action dispatch:
export const myAction = new Subject<string>()
) are meant to be called from outside the moduleNow, as for your root/branch/trunk separation of the state, I want to point you to a - sadly not yed documented feature - of reactive-state
. I had similar issues with structuring the state, and even using .createSlice()
turned out to be not enough for truly decoupled states between individual components/modules that occur in large-scale applications. Thats why I created the concept of projections and added it last month silently to the reactive-state codebase (present in reactive-state >=3.0).
As it is yet undocumented, here only a brief overview. The idea is not specific to reactive-state
but could be used in other state containers. Although, I am not aware of any that uses this concept and I came up with it myself.
Right now, .createSlice()
only operates on a property of a state, using a string/key to identify the property. For example: new Store({ subslice: "foo" }).createSlice("subslice")
.
The concept of projections are way more powerful: Given a state of type A
you can create any kind of slice of type B
by specifying a forward- and a backwardprojection. The forward projection is a function
that take a state of type A
and would return a state of type B
. The backward projection reverses this, as it is a function
that takes a state of type B
as argument, and returns a state of type A
as a result.
Here is some code to explain:
export interface Customer {
customerId: string
customerOrders: { orderId: string, product: string }[]
}
export type Order = { customerId: string, orderId: string, product: string }
const customerStore: Store<Customer> = Store.create({
customerId: "1",
customerOrders: [
{ orderId: "1", product: "banana" },
{ orderId: "2", product: "apple" }
]
})
const forwardProjection: (customer: Customer) => Order[] = (customer) => {
return customer.customerOrders.map(order => ({ customerId: customer.customerId, ...order }))
};
const backwardProjection: (orders: Order[]) => Customer = (orders) => {
return {
customerId: orders[0].customerId,
customerOrders: orders.map(({ orderId, product }) => ({ orderId, product }))
}
}
const orderStore: Store<Order[]> = customerStore.createProjection(forwardProjection, backwardProjection);
Now, I know the example is a bit lightweight and the code is probably hard to grasp but it shows the concept. We have two different state, Customer
and Order[]
. You can add reducers on both, the customerStore
and the orderStore
. Both reducer functions would have different signatures, as the state type is different for both stores. But since they are linked and automatically kept in-sync, whenever a reducer changes either store, the other store will reflect this change and fire all updates etc.
A slice state can be anything you want, not only a property of the (root) state. As long as you can transform the "parent" state A
into B
(=forward projection) and reverse this action (=backward projection) you can perfectly mix-and-match all data that you need for your module on the state. This allows maximum flexibility when designing the state object.
I know the concept of projections is very advanced and probably overkill for 90% of the applications, but it might be of use to anyone. Actually, the createSlice
function is implemented already using createProjection
internally: https://github.com/Dynalon/reactive-state/blob/master/src/store.ts#L139 and plenty of tests exists that might help in understanding it: https://github.com/Dynalon/reactive-state/blob/master/test/test_slicing.ts#L137
Note to self: This issue is still open and considered "enhancement". But I still need to decide whether a .addSideEffect()
is a good idea, as it might promote string-based actions (for the wrong reasons). Community feedback and discussion welcome, of course.
Thanks for the great work.
I try to dispatch the action manually but it call the reducer directly, the action didn't been called. I check out the source and see every addReducer will return the subscribe with the current action but dont't know why it isn't been called.
code example(edit from the demo) as follows:
` const dispatchProxy = async (payload: any, namespace: string) => {
}
const actionMap: ActionMap = {
} `