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)
}
},
Related
I have a reducer for state management at the context API. I was saving my Todos and it's happening successfully but when ı refresh the page all todos is deleting and just stay empty array.
// The relevant part in the reducer.
case "TODO_ADD_USER":
return {
...state,
users: action.payload,
};
// Localstorage functions.
useEffect(() => {
saveLocalTodos();
}, [state]);
useEffect(() => {
const localTodos = getLocalTodos();
dispatch({ type: "TODO_ADD_USER", payload: localTodos });
}, []);
const saveLocalTodos = () => {
if (localStorage.getItem("users") === null) {
localStorage.setItem("users", JSON.stringify([]));
} else {
localStorage.setItem("users", JSON.stringify(state.users));
}
};
const getLocalTodos = () => {
let locals;
if (localStorage.getItem("users") === null) {
locals = [];
} else {
locals = JSON.parse(localStorage.getItem("users"));
}
return locals;
};
Place of keeping the state.
const users = {
users: [],
};
There are a couple issues with your code.
The biggest one here is that you are saving the todos before getting them. So at the start of the application, things are getting reset, which is problematic.
Up next, you have your condition for the saving a bit backwards. You want to check if state.users === null and do a special action for that, rather than if localStorage.getItem("users") === null, as that will be null by default, and have nothing to do with the value in memory.
In fact, if the localStorage value is not null, but the state.users is, then it would set "null" to localStorage, which is less than ideal.
Here's the working code:
useEffect(() => {
// Get the item from local storage. JSON.parse(null) returns null rather than throws
// Get from local storage before setting it
const localTodos = JSON.parse(localStorage.getItem("users")) || [];
dispatch({ type: "TODO_ADD_USER", payload: localTodos });
}, []);
useEffect(() => {
// The conditions for this was wrong.
// You want to check if `state.users` has a value
// If it does, store! If not, don't store.
// That way you don't reset data
// In the case that you have this app running in two windows,
// there's more that needs to be done for that.
if (state.users) {
localStorage.setItem("users", JSON.stringify(state.users || []));
}
}, [state]);
https://codesandbox.io/s/angry-glitter-9l10t?file=/src/App.js
I have a three-check box type,
When I check any box I call refetch() in useEffect().
The first time, I check all boxes and that returns the expected data!
but for some cases "rechange the checkboxes randomly", the returned data from API is "undefined" although it returns the expected data in Postman!
So I Guess should I need to provide a unique queryKey for every data that I want to fetch
so I provide a random value "Date.now()" but still return undefined
Code snippet
type bodyQuery = {
product_id: number;
values: {};
};
const [fetch, setFetch] = useState<number>();
const [bodyQuery, setBodyQuery] = useState<bodyQuery>({
product_id: item.id,
values: {},
});
const {
data: updatedPrice,
status,
isFetching: loadingPrice,
refetch,
} = useQuery(
['getUpdatedPrice', fetch, bodyQuery],
() => getOptionsPrice(bodyQuery),
{
enabled: false,
},
);
console.log('#bodyQuery: ', bodyQuery);
console.log('#status: ', status);
console.log('#updatedPrice: ', updatedPrice);
useEffect(() => {
if (Object.keys(bodyQuery.values).length > 0) {
refetch();
}
}, [bodyQuery, refetch]);
export const getOptionsPrice = async (body: object) => {
try {
let response = await API.post('/filter/product/price', body);
return response.data?.detail?.price;
} catch (error) {
throw new Error(error);
}
};
So after some elaboration in the chat, this problem can be solved by leveraging the useQuery key array.
Since it behaves like the dependency array in the useEffect for example, everything that defines the resulted data should be inserted into it. Instead of triggering refetch to update the data.
Here the key could look like this: ['getUpdatedPrice', item.id, ...Object.keys(bodyQuery.values)], which will trigger a new fetch if those values change and on initial render.
Im trying to figure this out.
I want to get all my users from my database, cache them
and then when making a new request I want to get those that Ive cached + new ones that have been created.
So far:
const batchUsers = async ({ user }) => {
const users = await user.findAll({});
return users;
};
const apolloServer = new ApolloServer({
schema,
playground: true,
context: {
userLoader: new DataLoader(() => batchUsers(db)),// not sending keys since Im after all users
},
});
my resolver:
users: async (obj, args, context, info) => {
return context.userLoader.load();
}
load method requiers a parameter but in this case I dont want to have a specific user I want all of them.
I dont understand how to implement this can someone please explain.
If you're trying to just load all records, then there's not much of a point in utilizing DataLoader to begin in. The purpose behind DataLoader is to batch multiple calls like load(7) and load(22) into a single call that's then executed against your data source. If you need to get all users, then you should just call user.findAll directly.
Also, if you do end up using DataLoader, make sure you pass in a function, not an object as your context. The function will be ran on each request, which will ensure you're using a fresh instance of DataLoader instead of one with a stale cache.
context: () => ({
userLoader: new DataLoader(async (ids) => {
const users = await User.findAll({
where: { id: ids }
})
// Note that we need to map over the original ids instead of
// just returning the results of User.findAll because the
// length of the returned array needs to match the length of the ids
return ids.map(id => users.find(user => user.id === id) || null)
}),
}),
Note that you could also return an instance of an error instead of null inside the array if you want load to reject.
Took me a while but I got this working:
const batchUsers = async (keys, { user }) => {
const users = await user.findAll({
raw: true,
where: {
Id: {
// #ts-ignore
// eslint-disable-next-line no-undef
[op.in]: keys,
},
},
});
const gs = _.groupBy(users, 'Id');
return keys.map(k => gs[k] || []);
};
const apolloServer = new ApolloServer({
schema,
playground: true,
context: () => ({
userLoader: new DataLoader(keys => batchUsers(keys, db)),
}),
});
resolver:
user: {
myUsers: ({ Id }, args, { userLoader }) => {
return userLoader.load(Id);
},
},
playground:
{users
{Id
myUsers
{Id}}
}
playground explained:
users basically fetches all users and then myusers does the same thing by inhereting the id from the first call.
I think I choose a horrible example here since I did not see any gains in performence by this. I did see however that the query turned into:
SELECT ... FROM User WhERE ID IN(...)
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
}
}
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.