Redux.js and data with relations - javascript

Imagine that you develop some react-redux application (with global immuatable tree-state). And some data have some rules-relations in different tree-branches, like SQL relations between tables.
I.e. if you are working on some company's todos list, each todo has relation(many-to-one) with concrete user. And if you add some new user, you should add empty todo list (to other branch in the state). Or delete user means that you should re-assign user's todos to some (default admin) user.
You can hardcode this relation directly to source code. And it is good and works OK.
But imagine that you have got million small relations for data like this. It will be good that some small "automatic" operations/checks (for support/guard relations) performs automatically according to rules.
May be existed some common approach/library/experience to do it via some set of rules: like triggers in SQL:
on add new user => add new empty todos
on user delete => reassign todos to default user

There are two solutions here. I don't think that you should aim to have this kind of functionality in a redux application, so my first example is not quite what you're looking for but I think is more conical. The second example adopts a DB/orm pattern, which may work fine, but is not conical, and requires
These could be trivially added safely with vanilla redux and redux-thunk. Redux thunk basically allows you to dispatch a single action that its self dispatches multiple other actions--so when you trigger CREATE_USER, just do something along the lines of triggering CREATE_EMPTY_TODO, CREATE_USER, and ASSIGN_TODO in the createUser action. For deleting users, REASSIGN_USER_TODOS and then DELETE_USER.
For the examples you provide, here are examples:
function createTodoList(todos = []) {
return dispatch => {
return API.createTodoList(todos)
.then(res => { // res = { id: 15543, todos: [] }
dispatch({ type: 'CREATE_TODO_LIST_SUCCESS', res });
return res;
});
}
}
function createUser (userObj) {
return dispatch => {
dispatch(createTodoList())
.then(todoListObj => {
API.createUser(Object.assign(userObj, { todoLists: [ todoListObj.id ] }))
.then(res => { // res = { id: 234234, name: userObj.name, todoLists: [ 15534 ]}
dispatch({ type: 'CREATE_USER_SUCCESS', payload: res });
return res;
})
})
.catch(err => console.warn('Could not create user because there was an error creating todo list'));
}
}
Deleteing, sans async stuff.
function deleteUser (userID) {
return (dispatch, getState) => {
dispatch({
type: 'REASSIGN_USER_TODOS',
payload: {
fromUser: userID,
toUser: getState().application.defaultReassignUser
});
dispatch({
type: 'DELETE_USER',
payload: { userID }
});
}
}
The problem with this method, as mentioned in the comments, is that a new developer might come onto the project without knowing what actions already exist, and then create their own version of createUser which doesn't know to create todos. While you can never completely take away their ability to write bad code, you can try to be more defensive by making your actions more structured. For example, if your actions look like this:
const createUserAction = {
type: 'CREATE',
domain: 'USERS',
payload: userProperies
}
you can have a reducer structured like this
function createUserTrigger (state, userProperies) {
return {
...state,
todoLists: {
...state.todoLists,
[userProperies.id]: []
}
}
}
const triggers = {
[CREATE]: {
[USERS]: createUserTrigger
}
}
function rootReducer (state = initialState, action) {
const { type, domain, payload } = action;
let result = state;
switch (type) {
case CREATE:
result = {
...state,
[domain]: {
...state[domain],
[payload.id]: payload
}
};
break;
case DELETE:
delete state[domain][payload.id];
result = { ...state };
break;
case UPDATE:
result = {
...state,
[domain]: {
...state[domain],
[payload.id]: _.merge(state[domain][payload.id], payload)
}
}
break;
default:
console.warn('invalid action type');
return state;
}
return triggers[type][domain] ? triggers[type][domain](result, payload) : result;
}
In this case, you're basically forcing all developers to use a very limited possible set of action types. Its very rigid and I don't really recommend it, but I think it does what you're asking.

Related

How to dispatch an action from within another action in another Vuex store module?

CONTEXT
I have two store modules : "Meetings" and "Demands".
Within store "Demands" I have "getDemands" action, and within store "Meetings" I have "getMeetings" action. Prior to access meetings's data in Firestore, I need to know demands's Id (ex.: demands[i].id), so "getDemands" action must run and complete before "getMeetings" is dispatched.
Vuex documentation dispatching-action is very complete, but still, I don't see how to fit it in my code. There are also somme other good answered questions on the topic here :
Vue - call async action only after first one has finished
Call an action from within another action
I would like to know the best way to implement what I'm trying to accomplish. From my perspective this could be done by triggering one action from another, or using async / await, but I'm having trouble implementing it.
dashboard.vue
computed: {
demands() {
return this.$store.state.demands.demands;
},
meetings() {
return this.$store.state.meetings.meetings;
}
},
created() {
this.$store.dispatch("demands/getDemands");
//this.$store.dispatch("meetings/getMeetings"); Try A : Didn't work, seems like "getMeetings" must be called once "getDemands" is completed
},
VUEX store
Module A – demands.js
export default {
namespaced: true,
state: {
demands:[], //demands is an array of objects
},
actions: {
// Get demands from firestore UPDATED
async getDemands({ rootState, commit, dispatch }) {
const { uid } = rootState.auth.user
if (!uid) return Promise.reject('User is not logged in!')
const userRef = db.collection('profiles').doc(uid)
db.collection('demands')
.where('toUser', "==", userRef)
.get()
.then(async snapshot => {
const demands = await Promise.all(
snapshot.docs.map(doc =>
extractDataFromDemand({ id: doc.id, demand: doc.data() })
)
)
commit('setDemands', { resource: 'demands', demands })
console.log(demands) //SECOND LOG
})
await dispatch("meetings/getMeetings", null, { root: true }) //UPDATE
},
...
mutations: {
setDemands(state, { resource, demands }) {
state[resource] = demands
},
...
Module B – meetings.js
export default {
namespaced: true,
state: {
meetings:[],
},
actions: {
// Get meeting from firestore UPDATED
getMeetings({ rootState, commit }) {
const { uid } = rootState.auth.user
if (!uid) return Promise.reject('User is not logged in!')
const userRef = db.collection('profiles').doc(uid)
const meetings = []
db.collection('demands')
.where('toUser', "==", userRef)
.get()
.then(async snapshot => {
await snapshot.forEach((document) => {
document.ref.collection("meetings").get()
.then(async snapshot => {
await snapshot.forEach((document) => {
console.log(document.id, " => ", document.data()) //LOG 3, 4
meetings.push(document.data())
})
})
})
})
console.log(meetings) // FIRST LOG
commit('setMeetings', { resource: 'meetings', meetings })
},
...
mutations: {
setMeetings(state, { resource, meetings }) {
state[resource] = meetings
},
...
Syntax:
dispatch(type: string, payload?: any, options?: Object): Promise<any
Make the call right
dispatch("meetings/getMeetings", null, {root:true})

Best practise to combine multiple rest calls to populate 1 graphQL type in apollo-server

I have graphql User type that needs information from multiple REST api's and different servers.
Basic example: get the user firstname from rest domain 1 and get lastname from rest domain 2. Both rest domain have a common "userID" attribute.
A simplefied example of my resolver code atm:
user: async (_source, args, { dataSources }) => {
try {
const datasource1 = await dataSources.RESTAPI1.getUser(args.id);
const datasource2 = await dataSources.RESTAPI2.getUser(args.id);
return { ...datasource1, ...datasource2 };
} catch (error) {
console.log("An error occurred.", error);
}
return [];
}
This works fine for this simplefied version, but I have 2 problems with this solution:
first, IRL there is a lot of logic going into merging the 2 json results. Since some field are shared but have different data (or are empty). So it's like cherry picking both results to create a combined result.
My second problem is that this is still a waterfall method. First get the data from restapi1, when thats done call restapi2. Basicly apollo-server is reintroducing rest-waterfall-fetch graphql tries to solve.
Keeping these 2 problems in mind.. Can I optimise this piece of code or rewrite is for better performance or readability? Or are there any packages that might help with this behavior?
Many thanks!
With regard to performance, if the two calls are independent of one another, you can utilize Promise.all to execute them in parallel:
const [dataSource1,dataSource2] = await Promise.all([
dataSources.RESTAPI1.getUser(args.id),
dataSources.RESTAPI2.getUser(args.id),
])
We normally let GraphQL's default resolver logic do the heavy lifting, but if you're finding that you need to "cherry pick" the data from both calls, you can return something like this in your root resolver:
return { dataSource1, dataSource2 }
and then write resolvers for each field:
const resolvers = {
User: {
someField: ({ dataSource1, dataSource2 }) => {
return dataSource1.a || dataSource2.b
},
someOtherField: ({ dataSource1, dataSource2 }) => {
return someCondition ? dataSource1.foo : dataSource2.bar
},
}
}
Assuming your user resolver returns type User forsake...
type User {
id: ID!
datasource1: RandomType
datasource1: RandomType
}
You can create individual resolvers for each field in type User, this can reduce the complexity of the user Query, to only the requested fields.
query {
user {
id
datasource1 {
...
}
}
}
const resolvers = {
Query: {
user: () => {
return { id: "..." };
}
},
User: {
datasource1: () => { ... },
datasource2: () => { ... } // i wont execute
}
};
datasource1 & datasource2 resolvers will only execute in parallel, after Query.user executes.
For parallel call.
const users = async (_source, args, { dataSources }) => {
try {
const promises = [
dataSources.RESTAPI1,
dataSources.RESTAPI2
].map(({ getUser }) => getUser(args.id));
const data = await Promise.all(promises);
return Object.assign({}, ...data);
} catch (error) {
console.log("An error occurred.", error);
}
return [];
};

Vue, Vuex, JavaScript: includes() does not work as expected

I wanna store some objects inside an array if the array doesn't already contain some object with the same id. anyways, everything works fine til i start adding more than one object at a time.
Here is the related code using Vuex:
// filter function to check if element is already included
function checkForDuplicate(val) {
for( let sessionItem of state.sessionExercises ) {
return sessionItem._id.includes(val._id);
}
};
// related array from vuex state.js
sessionExercises: [],
// vuex mutation to store exercises to session exercises
storeSessionExercises: (state, payload) => {
// Pre filtering exercises and prevent duplicated content
if( checkForDuplicate(payload) === true ) {
console.log("Exercise ist bereits für session registriert!");
} else {
state.sessionExercises.push(payload);
}
},
// Related vuex action
storeSessionExercises: ({ commit }, payload) => {
commit("storeSessionExercises", payload)
},
As I wrote before everything works fine as long i ad a single object, checkForDuplicate() will find duplicated objects and deny a push to the array.
now there is a case in which I wanna push a bundle of objects to the array, which i am doing through an database request, looping through the output, extracting the objects and pushing them through the same function as I do with the single objects:
// get user related exercises from database + clear vuex storage + push db-data into vuex storage
addSessionWorkout: ({ commit, dispatch }, payload) => {
axios.post(payload.apiURL + "/exercises/workout", payload.data, { headers: { Authorization: "Bearer " + payload.token } })
.then((result) => {
// loop through output array and
for( let exercise of result.data.exercises ) {
// push (unshift) new exercise creation to userExercises array of vuex storage
dispatch("storeSessionExercises", exercise)
};
})
.catch((error) => {
console.error(error)
});
},
The push does also work as it should, the "filter function" on the other hand doesn't do its job. It will filter the first object and deny to push it to the array, but if there is a second one that one will be pushed to the array even inf the same object (same Id) is already included, what am I not seeing here!? makes me nuts! :D
I understand it like the loop will put each object through the checkForDuplicate() and look if there is an duplicate it should output true, so the object doesn't get pushed into the array. If anybody sees what I currently don't just let me know.
the mistake is your filter function. you want to loop over your sessionExercises and only return true if any of them matches. However, at the moment you return the result of the very first check. Your loop will always only run one single time.
Option 1: only return if matched
function checkForDuplicate(val) {
for( let sessionItem of state.sessionExercises ) {
if (sessionItem._id.includes(val._id)) {
return true;
}
}
return false;
};
Option 2: use es6 filter
storeSessionExercises: (state, payload) => {
var exercises = state.sessionExercises.filter(ex => (ex._id.includes(payload._id)));
if(exercises.length) {
console.log("Exercise ist bereits für session registriert!");
} else {
state.sessionExercises.push(payload);
}
}
I would change the addSessionWorkout action, I would create a new exercises array with the old and new entries and then update the state.
// related array from vuex state.js
sessionExercises: [],
// vuex mutation to store exercises to session exercises
storeSessionExercises: (state, payload) => {
state.sessionExercises = payload;
},
// Related vuex action
storeSessionExercises: ({ commit }, payload) => {
commit("storeSessionExercises", payload)
},
addSessionWorkout: async({
commit,
dispatch,
state
}, payload) => {
const result = await axios.post(payload.apiURL + "/exercises/workout", payload.data, {
headers: {
Authorization: "Bearer " + payload.token
}
})
try {
const newExercices = result.data.exercises.reduce((acc, nextItem) => {
const foundExcercise = acc.find(session => session.id === nextItem.id)
if (!foundExcercise) {
return [...acc, nextItem]
}
return acc
}, state.sessionExercises)
dispatch("storeSessionExercises", foundExcercise)
} catch (e) {
console.error(error)
}
},

State Variable triggers mutation error

I have the following pseudo-code in my store module
const state = {
users: []
}
const actions = {
addUsers: async ({commit, state}, payload) => {
let users = state.users // <-- problem
// fetching new users
for(let i of newUsersThatGotFetched) {
users.push('user1') // <-- really slow
}
commit('setUsers',users)
}
}
const mutations = {
setUsers: (state, { users }) => {
Vue.set(state, 'users', users)
}
}
Now - when I run this code, I get the following error Error: [vuex] Do not mutate vuex store state outside mutation handlers.
When I put strict mode to false - the error is gone - but the process-time is really, really slow - as if the errors still happen but without getting displayed.
The problem seems to be where I commented // <-- problem, because after I change that line to
let users = []
everything runs flawlessly, but I can't have that because I need the data of state.users
The problem is: users.push('user1'), this is the line that mutates the state.
Remove anything that mutates the state (writes or changes it) from actions and move that into a mutation.
addUsers: async ({ commit }, payload) => {
// fetching new users
commit('setUsers', newUsersThatGotFetched)
}
Then add the new users in the mutation.
const mutations = {
setUsers: (state, users) => {
state.users.concat(users);
// or if you have custom logic
users.forEach(user => {
if (whatever) state.users.push(user)
});
}
}
The reason it is slow is related to Strict mode
Strict mode runs a synchronous deep watcher on the state tree for detecting inappropriate mutations, and it can be quite expensive when you make large amount of mutations to the state. Make sure to turn it off in production to avoid the performance cost.
If you want to speed up the mutation, you could do the changes on a new array which would replace the one in the state when ready.
const mutations = {
setUsers: (state, newUsers) => {
state.users = newUsers.reduce((users, user) => {
if (whatever) users.push(user);
return users;
}, state.users.slice()); // here, we start with a copy of the array
}
}

redux - when a resource depends on another, how to update them appropriately

Say I have a redux state tree like this:
{
user: null,
purchases: [],
}
The purchases belong to a user, so I always want to update the purchases when the user is updated (though purchases can be updated at other times, too).
How does one go about keeping the purchases up to date as the user changes? I could perhaps dispatch an update of the purchases inside the action creator for the update to the user, but then I foresee adding dispatches to the fetchUser action creator every time I add a resource that depends on the current user, which seems redundant.
The code for my current action creators is below (note that I use thunk middleware):
Action creators for fetching the user:
export const fetchUserSuccess = user => ({
type: FETCH_USER_SUCCESS,
user,
});
export const fetchUserFailure = () => ({
type: FETCH_USER_FAILURE,
});
export const fetchUser = () => {
return dispatch => {
return getLoggedInUser(resp => {
const { user } = resp;
return (
user
? dispatch(fetchUserSuccess(user))
: dispatch(fetchUserFailure())
);
});
}
};
Action creators for fetching the purchases:
export const fetchPurchasesSuccess = purchases => ({
type: FETCH_PURCHASES_SUCCESS,
purchases,
});
export const fetchPurchasesFailure = () => ({
type: FETCH_PURCHASES_FAILURE,
});
export const fetchPurchases = user => {
return dispatch => {
return getPurchases(user, resp => {
const { purchases } = resp;
return (
purchases
? dispatch(fetchPurchasesSuccess(purchases))
: dispatch(fetchPurchasesFailure()));
});
}
};
Your purchases reducer could be 'listening' to some specific user's actions type like f.e. FETCH_USER_SUCCESS and then updating that resource directly in there.
Sorry for my English.
I think you have to pass user with purchase details.

Categories

Resources