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.
Related
I'm working on a small note taking app with React and Node.js (Express). If the user is authenticated I make API calls to the backend to fetch, create, update, delete notes persisted in a MongoDB database. If he's not, the notes are stored in localStorage. I have an AuthContext with login, logout and signup functions.
I can know if the user is loggedIn with my useAuth() custom hook in my AuthContext :
const { user } = useAuth();
And I have a separate file to make the API calls that I use in my components (getNotes, createNotes ...)
I fetch my notes in the useEffect hook
React.useEffect(() => {
const notes: Note[] = getNotes();
setNotes(notes);
}, []);
And I render my notes like this (simplified)
{notes.length > 0 && (
<ul>{notes.map(renderNote)}</ul>
)}
const renderNote = (note) => {
return (
<Note note={note} />
);
};
My question is what would be a good practice to implement the different behaviors (API calls or localStorage) ?
I can add a parameter isLoggedIn to the functions and add an if statement inside the function like this (simplified version) :
const getNotes = (isLoggedIn) => {
if (isLoggedIn) {
return notes = fetch("/notes")
} else {
return notes = localStorage.getItem("notes")
}
}
But this does not look like something clean to do if I have do to this in every function.
Thanks in advance for your help
Here's something you could do. I think I'd suggest you create the idea of some store that implements a simple getter/setter interface, then have your useAuth hook return the correct store depending on the auth state. If authenticated, then your hook returns the remote store. If not, then it returns the local storage store. But your store looks the same to your component no matter whether it's a local or remote store.
Now your code can just call get/set on the store and not care about where your info is stored or even whether the user is logged in. A main goal is to avoid having a lot of if (loggedIn) { ... } code all over your app.
Something like...
const useLocalStorageStore = () => {
const get = (key) => {
return localStorage.getItem(key);
};
const set = (key, value) => {
// I append 'local' here just to make it obvious the
// local store is in use in this example
localStorage.setItem(key, `${value} local`);
};
return { get, set };
};
// This contrived example uses localStorage too to make my example easier,
// but you'd add the fetch business to your get/set methods
// here in this remote store.
const useRemoteStore = () => {
const baseUrl = "http://localhost/foo/bar";
const get = async (key) => {
//return fetch(`${baseUrl}/${key}`);
// really should fetch here, but for this example use local
return localStorage.getItem(key);
};
const set = async (key, value) => {
// I append 'remote' here just to make it obvious the
// remote store is in use in this example
localStorage.setItem(key, `${value} remote`);
};
return { get, set };
};
const useAuth = () => {
// AuthContext is your source of loggedIn info,
// however you have it available in your app.
const { login, logout, loggedIn } = React.useContext(AuthContext);
const authedStore = useRemoteStore();
const unauthedStore = useLocalStorageStore();
const store = loggedIn ? authedStore : unauthedStore;
return { login, logout, loggedIn, store };
};
At this point, the store has all you need to get or set key values. Then you can use it in your components with...
const MyComponent = () => {
const { loggedIn, login, logout, store } = useAuth();
const setNotesValue = async (value) => {
// Your store handles communicating with the correct back end.
await store.set("notes", value);
};
const getNotesValue = async () => {
// Your store handles communicating with the correct back end.
const value = await store.get("notes");
};
return (
<div>Your UI...</div>
);
};
Here's a sandbox to demo this: https://codesandbox.io/s/gallant-gould-v1qe0
I have the user inside localstorage, when user logouts the localstorage data becomes NULL. When user logins, the localstorages fills with user's data but to check this my userEffect in App.js do not reflect any change.
i have signUp
dispatch(signin(form, history));
history.push("/"); //go back to App.js file
in Navbar the user data changes
const Navbar = (props) => {
const [user, setUser] = useState(JSON.parse(localStorage.getItem("profile")));
const logout = () => {
dispatch({ type: "LOGOUT" });
dispatch({
type: "EMPTY_CART",
});
history.push("/");
setUser(null);
};
now at App.js i have
const user = JSON.parse(localStorage?.getItem("profile"));
const getCartItems = useCallback(async () => {
if (user) {
console.log("Yes user exixts");
dispatch(getEachUserCart(user?.result?._id));
} else {
console.log("No user exist");
}
}, []); //
useEffect(() => {
getCartItems();
}, [getCartItems]);
Now if u look above, after dispatching signUp action, i come back to App.js but here the useEffect don't run nor it checks if user have changed.
Hey – looks like you have a missing dependency issue. You need to have it like so
const getCartItems = useCallback(async () => {
if (user) {
console.log("Yes user exixts");
dispatch(getEachUserCart(user?.result?._id));
} else {
console.log("No user exist");
}
}, [user]);
Otherwise, this function will never be redeclared with the latest user value. Kindly let me know if that helps
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})
I'm using nextjs and apollo (with react hooks). I am trying to update the user object in the apollo cache (I don't want to refetch). What is happening is that the user seems to be getting updated in the cache just fine but the user object that the component uses is not getting updated. Here is the relevant code:
The page:
// pages/index.js
...
const Page = ({ user }) => {
return <MyPage user={user} />;
};
Page.getInitialProps = async (context) => {
const { apolloClient } = context;
const user = await apolloClient.query({ query: GetUser }).then(({ data: { user } }) => user);
return { user };
};
export default Page;
And the component:
// components/MyPage.jsx
...
export default ({ user }) => {
const [toggleActive] = useMutation(ToggleActive, {
variables: { id: user.id },
update: proxy => {
const currentData = proxy.readQuery({ query: GetUser });
if (!currentData || !currentData.user) {
return;
}
console.log('user active in update:', currentData.user.isActive);
proxy.writeQuery({
query: GetUser,
data: {
...currentData,
user: {
...currentData.user,
isActive: !currentData.user.isActive
}
}
});
}
});
console.log('user active status:', user.isActive);
return <button onClick={toggleActive}>Toggle active</button>;
};
When I continuously press the button, the console log in the update function shows the user active status as flipping back and forth, so it seems that the apollo cache is getting updated properly. However, the console log in the component always shows the same status value.
I don't see this problem happening with any other apollo cache updates that I'm doing where the data object that the component uses is acquired in the component using the useQuery hook (i.e. not from a query in getInitialProps).
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.