Yo! I'm using Redux and Normalizr. The API I'm working with sends down objects that look like this:
{
name: 'Foo',
type: 'ABCD-EFGH-IJKL-MNOP'
}
or like this
{
name: 'Foo2',
children: [
'ABCD-EFGH-IJKL-MNOP',
'QRST-UVWX-YZAB-CDEF'
]
}
I want to be able to asynchronously fetch those related entities (type and children) when the above objects are accessed from the state (in mapStateToProps). Unfortunately, this does not seem to mesh with the Redux way as mapStateToProps is not the right place to call actions. Is there an obvious solution to this case that I'm overlooking (other than pre-fetching all of my data)?
Not sure that I have correctly understood your use-case, but if you want to fetch data, one simple common way is to trigger it from a React component:
var Component = React.createClass({
componentDidMount: function() {
if (!this.props.myObject) {
dispatch(actions.loadObject(this.props.myObjectId));
}
},
render: function() {
const heading = this.props.myObject ?
'My object name is ' + this.props.myObject.name
: 'No object loaded';
return (
<div>
{heading}
</div>
);
},
});
Given the "myObjectId" prop, the component triggers the "myObject" fetching after mounting.
Another common way would be to fetch the data, if it's not already here, from a Redux async action creator (see Redux's doc for more details about this pattern):
// sync action creator:
const FETCH_OBJECT_SUCCESS = 'FETCH_OBJECT_SUCCESS';
function fetchObjectSuccess(objectId, myObject) {
return {
type: FETCH_OBJECT_SUCCESS,
objectId,
myObject,
};
}
// async action creator:
function fetchObject(objectId) {
return (dispatch, getState) => {
const currentAppState = getState();
if (!currentAppState.allObjects[objectId]) {
// fetches the object if not already present in app state:
return fetch('some_url_.../' + objectId)
.then(myObject => (
dispatch(fetchObjectSuccess(objectId, myObject))
));
} else {
return Promise.resolve(); // nothing to wait for
}
};
}
Related
I have an object in my pinia store like
import { defineStore } from "pinia";
export const useSearchStore = defineStore("store", {
state: () => {
return {
myobj: {
foo: 0,
bar: 2000,
too: 1000,
},
};
},
getters: {
changed() {
// doesn't work
return Object.entries(this.myobj).filter(([key, value]) => value != initialvalue
);
},
},
});
How do I get the initial value to test if the object changed. Or how can I return a filtered object with only those entries different from initial state?
My current workaround:
in a created hook I make a hard copy of the store object I then can compare to. I guess there is a more elegant way...
I had done this (although I do not know if there a better way to avoid cloning without duplicating your initial state).
Define your initial state outside and assign it to a variable as follows;
const initialState = {
foo: 0,
bar: 2000,
too: 1000
}
Then you can use cloning to retain the original state;
export const useSearchStore = defineStore("store", {
state: () => {
return {
myobj: structuredClone(initialState),
};
},
getters: {
changed: (state) => deepEquals(initialState, state.myobj);
},
});
where deepEquals is a method which deep compares the two objects (which you would have to implement). I would use lodash (npm i lodash and npm i #types/lodash --save-dev if you're using TypeScript) for this.
Full code (with lodash);
import { defineStore } from "pinia";
import { cloneDeep, isEqual } from "lodash";
const initialState = {
foo: 0,
bar: 2000,
too: 1000
}
export const useSearchStore = defineStore("store", {
state: () => ({
myobj: cloneDeep(initialState)
}),
getters: {
changed(state) {
return isEqual(initialState, state.myobj);
},
},
});
If you also want the differences between the two you can use the following function (the _ is lodash - import _ from "lodash");
function difference(object, base) {
function changes(object, base) {
return _.transform(object, function (result: object, value, key) {
if (!_.isEqual(value, base[key])) {
result[key] =
_.isObject(value) && _.isObject(base[key])
? changes(value, base[key])
: value;
}
});
}
return changes(object, base);
}
courtesy of https://gist.github.com/Yimiprod/7ee176597fef230d1451
EDIT:
The other way you would do this is to use a watcher to subscribe to changes. The disadvantage to this is that you either have to be OK with your state marked as "changed" if you change back the data to the initial state. Otherwise, you would have to implement a system (perhaps using a stack data structure) to maintain a list of changes so that if two changes which cancel each other out occur then you would remark the state as "unchanged". You would have to keep another variable (boolean) in the state which holds whether the state has been changed/unchanged - but this would be more complicated to implement and (depending on your use case) not worth it.
I started using ngrx/entity package, where I can manage store by adapter. There is addOne method I'd like to use, but it adds item to the end of collection. I wanna add one at the beginning. Could you please help me with that? How to add item at the beginning with EntityAdapter.
How I create entity adapter:
export const adapter: EntityAdapter<AssetTreeNode> = createEntityAdapter({
selectId: (model: AssetTreeNode) => model.Id
});
Reducer looks like that:
export function reducer(state: AssetListState = initialState, action: AssetListAction) {
switch (action.type) {
(...)
case ASSET_LIST_ADD_ITEM:
let assetToAdd: AssetTreeNode = Object.assign({} as AssetTreeNode,
action.payload.asset,
{ Id: action.payload.createdAssetId });
return adapter.addOne(assetToAdd, state); <--- I wanna add here at the end.
(...)
default:
return state;
}
}
There is no proper way provided by #ngrx/entity team. One of the answer mentions to use sort-comparator. But i believe using sort-comparator is not the right way to go. Suppose there is two click actions and in one action we need to append item below and in other action on top. here we will run into the same problem again.
I had run into the same issue and my solution to the problem is to reconstruct the list when we want the item on top of the list.
To add at the top of entity list
const { selectAll } = myAdapter.getSelectors();
...
...
on(MyActions.addItem, (state, { item }) =>{
return myAdapter.setAll([item ,...selectAll(state)], { ...state})
}),
To add at the bottom of entity list
on(MyActions.addItem, (state, { item}) =>{
return myAdapter.addOne(item, state)
}),
The only way to change this behavior would be to use the sortComparer when you create the adapter - docs.
export const adapter: EntityAdapter<User> = createEntityAdapter<User>({
sortComparer: (a: User, b: User) => a.name.localeCompare(b.name),
});
Maybe you could place the item at the begining and replace the list
on(addAsset, (state, { payload }) => {
const currentList = Object.values(state.entities);
const newList = [payload, ...currentList];
return adapter.setAll(newList, state);
});
I'm trying to subscribe to a subject. This is working as expected the first time but throwing the above error the second time and I can't see where to fix it.
export function uploadSceneFile(action$, store) {
return action$.ofType(CREATE_SCENE_SUCCESS)
.mergeMap(({payload}) =>
UploadSceneWithFile(payload)
.map(res => {
if (res.progress > 0){
return { type: UPLOAD_SCENE_PROGRESS, scene: res }
}
else if(res.progress === -1){
return { type: UPLOAD_SCENE_SUCCESS, scene: res }
}
})
)
}
It's designed to listen for the scen being created, dispatch upload progress notifications and then dispatch the success message.
The error gets thrown straight away from this line the second time it runs
onProgress: (val)=> subject$.next({...scene,progress:val}),
export function UploadSceneWithFile(scene){
const subject$ = new Subject()
scene.filename = scene.file.name
scene.type = scene.file.type.match('image') ? 0 : 1
FileToScenePreview(scene).then(res => {
scene.thumbName = res.thumbName
})
const uploader = new S3Upload({
getSignedUrl: getSignedUrl,
uploadRequestHeaders: {'x-amz-acl': 'public-read'},
contentType: scene.file.type,
contentDisposition: 'auto',
s3path: 'assets/',
onError:()=>subject$.next('error'),
onProgress: (val)=> subject$.next({...scene,progress:val}),
onFinishS3Put: ()=> {
subject$.next({...scene,progress:-1})
subject$.complete()
},
})
uploader.uploadFile(scene.file)
return subject$.asObservable()
}
ERROR MESSAGE
Subscriber.js:242 Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.
at Object.performAction (<anonymous>:1:40841)
at liftAction (<anonymous>:1:34377)
at dispatch (<anonymous>:1:38408)
at createEpicMiddleware.js:59
at createEpicMiddleware.js:59
at SafeSubscriber.dispatch [as _next] (applyMiddleware.js:35)
at SafeSubscriber../node_modules/rxjs/Subscriber.js.SafeSubscriber.__tryOrUnsub (Subscriber.js:238)
at SafeSubscriber../node_modules/rxjs/Subscriber.js.SafeSubscriber.next (Subscriber.js:185)
at Subscriber../node_modules/rxjs/Subscriber.js.Subscriber._next (Subscriber.js:125)
at Subscriber../node_modules/rxjs/Subscriber.js.Subscriber.next (Subscriber.js:89)
at SwitchMapSubscriber../node_modules/rxjs/operators/switchMap.js.SwitchMapSubscriber.notifyNext (switchMap.js:126)
at InnerSubscriber../node_modules/rxjs/InnerSubscriber.js.InnerSubscriber._next (InnerSubscriber.js:23)
at InnerSubscriber../node_modules/rxjs/Subscriber.js.Subscriber.next (Subscriber.js:89)
at MergeMapSubscriber../node_modules/rxjs/operators/mergeMap.js.MergeMapSubscriber.notifyNext (mergeMap.js:145)
at InnerSubscriber../node_modules/rxjs/InnerSubscriber.js.InnerSubscriber._next (InnerSubscriber.js:23)
at InnerSubscriber../node_modules/rxjs/Subscriber.js.Subscriber.next (Subscriber.js:89)
at MergeMapSubscriber../node_modules/rxjs/operators/mergeMap.js.MergeMapSubscriber.notifyNext (mergeMap.js:145)
at InnerSubscriber../node_modules/rxjs/InnerSubscriber.js.InnerSubscriber._next (InnerSubscriber.js:23)
at InnerSubscriber../node_modules/rxjs/Subscriber.js.Subscriber.next (Subscriber.js:89)
at MapSubscriber../node_modules/rxjs/operators/map.js.MapSubscriber._next (map.js:85)
at MapSubscriber../node_modules/rxjs/Subscriber.js.Subscriber.next (Subscriber.js:89)
at Subject../node_modules/rxjs/Subject.js.Subject.next (Subject.js:55)
at S3Upload.onProgress (uploadSceneFile.js:27)
at S3Upload.<anonymous> (s3upload.js:139)
In the inner map within your uploadSceneFile, you have an if statement followed by an else if statement, of if neither is true, the map will return undefined instead of an action.
.map(res => {
if (res.progress > 0){
return { type: UPLOAD_SCENE_PROGRESS, scene: res }
}
else if(res.progress === -1){
return { type: UPLOAD_SCENE_SUCCESS, scene: res }
}
// An action should be returned here!
})
Note that, when passed an undefined action, the check that Redux performs to determine whether or not an action is a plain object will effect the error you are seeing.
As far as my understanding goes, it's an anti-pattern to dispatch actions from within a store update handler. Correct?
How can I handle the following workflow then?
I have some company switcher on my page header
Clicking on a company dispatches some SELECTEDCOMPANY_UPDATE action
The active view reacts on the according change in the state store by forcing a data reload. E.g. by calling companyDataService.fetchOrders(companyName).
I'd like to show some loading animation during the data is being fetched and therefore have an dedicated action like FETCHINGDATA_UPDATE which updates the fetchingData section in my app state store to which all interested views can react by showing/hiding the load mask
Where do I actually dispatch the FETCHINGDATA_UPDATE action? If I directly do this from within companyDataService.fetchOrders(companyName) it would be called from within a store update handler (see OrdersView.onStoreUpdate in exemplary code below)...
Edit
To clarify my last sentence I'm adding some exemplary code which shows how my implementation would have looked like:
ActionCreator.js
// ...
export function setSelectedCompany(company) {
return { type: SELECTEDCOMPANY_UPDATE, company: company };
}
export function setFetchingData(isFetching) {
return { type: FETCHINGDATA_UPDATE, isFetching: isFetching };
}
// ...
CompanyDataService.js
// ...
export fetchOrders(companyName) {
this.stateStore.dispatch(actionCreator.setFetchingData(true));
fetchData(companyName)
.then((data) => {
this.stateStore.dispatch(actionCreator.setFetchingData(false));
// Apply the data...
})
.catch((err) => {
this.stateStore.dispatch(actionCreator.setFetchingData(false));
this.stateStore.dispatch(actionCreator.setFetchError(err));
})
}
// ...
CompanySwitcher.js
// ...
onCompanyClicked(company) {
this.stateStore.dispatch(actionCreator.setSelectedCompany(company));
}
// ...
OrdersView.js
// ...
constructor() {
this._curCompany = '';
this.stateStore.subscribe(this.onStoreUpdate);
}
// ...
onStoreUpdate() {
const stateCompany = this.stateStore.getState().company;
if (this._curCompany !== stateCompany) {
// We're inside a store update handler and `fetchOrders` dispatches another state change which is considered bad...
companyDataService.fetchOrders(stateCompany);
this._curCompany = stateComapny;
}
}
// ...
I agree with Davin, in the action creator is the place to do this, something like:
export function fetchOrders (company) {
return (dispatch) => {
dispatch ({ type: FETCHINGDATA_UPDATE });
return fetchOrderFunction ().then(
(result) => dispatch ({ type: FETCHING_COMPLETED, result }),
(error) => dispatch ({ type: FETCHING_FAILED, error })
);
};
}
Then in the reducer FETCHINGDATA_UPDATE can set your loading indicator to true and you can set it back to false I both SUCCESS and FAILED
When trying to read an attribute, meteor gives me a TypeError: Cannot read property 'featuredImage' of undefined error in the browser console. But it reads featuredImage and the site is working fine. How can I get rid of this error? Is it happening because my subscriptions are not yet ready? Is that's the case, how to fix it? (PS : Im using the flow router so I can't wait for subscriptions in the router)
My template code :
Template.About.helpers({
page: () => {
return findPage();
},
featuredImage: () => {
var thisPage = findPage();
return Images.findOne({
"_id": thisPage.featuredImage
});
}
});
function findPage() {
return Pages.findOne({
slug: 'about'
});
}
The router code :
FlowRouter.route('/about', {
name: 'about',
subscriptions: function() {
this.register('page', Meteor.subscribe('pages', 'about'));
this.register('image', Meteor.subscribe('images'));
},
action() {
BlazeLayout.render('MainLayout', {
content: 'About'
});
setTitle('About Us');
},
fastRender: true
});
The subscription is probably not ready yet. FlowRouter provides a utility for dealing with this, your helpers should look like this:
Template.About.helpers({
page: () => {
// If you only need a specific subscription to be ready
return FlowRouter.subsReady('page') && findPage() || null;
},
featuredImage: () => {
// Ensure ALL subscriptions are ready
if ( FlowRouter.subsReady() ) {
var thisPage = findPage();
return Images.findOne({
"_id": thisPage.featuredImage // Probably should be thisPage.featuredImage._id
});
}
return null;
}
});
However, for maximum performance, you should use if (FlowRouter.subsReady('page') && Flowrouter.subsReady('image')) rather than FlowRouter.subsReady() since if you have other pending subscriptions which are large, it will wait for those even though you don't need them.