-
Notifications
You must be signed in to change notification settings - Fork 7
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
Allow string-based dispatch for side-effects #15
Comments
Can you post the code that you used to register the actual action via 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: 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 The issue is that you are dispatching a dog's race name instead of an image url - the Now, when you dispatch through an observable, your are calling // 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, In your case I see several options that you have:
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
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 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
I don't see any boilerplate. The boilerplate will be magic string constant that exist throughout your application. You can put all To sum up the benefit of strictly using Observables/Subjects for action dispatch:
Managing / breaking down the state for modulesNow, as for your root/branch/trunk separation of the state, I want to point you to a - sadly not yed documented feature - of As it is yet undocumented, here only a brief overview. The idea is not specific to Right now, 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, 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 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 |
Note to self: This issue is still open and considered "enhancement". But I still need to decide whether a |
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 = {
}
`
The text was updated successfully, but these errors were encountered: