I am currently coding an application in VueJS (and with Vuex in particular). However, my question is not strongly linked to this library, but rather to the architecture to have with a store like flux/redux/Vuex.
To put it simply, I have several APIs (one API/database per team), and for each team/API, I have several users.These teams and users are represented by simple objects, and each has its own slug. Important note: the slugs of the teams are of course unique, but the slugs users are unique for their own team. The uniqueness constraint for a user would then be "teamSlug/userSlug". And given the large number of users, I can not simply load all the users of all the teams.
My question is how to properly architect my application/store in order to recover the data of a given user slug (with his team): if I have not already loaded this user, make an API request to retrieve it. Currently I have created a getter that returns the user object, which takes the slug from the user and the team. If it returns "null" or with a ".loading" to "false", I have to run the "loadOne" action that will take care of retrieving it:
import * as types from '../../mutation-types'
import users from '../../../api/users'
// initial state
const state = {
users: {}
}
// getters
const getters = {
getOne: state => (team, slug) => (state.users[team] || {})[slug] || null
}
// actions
const actions = {
loadOne ({ commit, state }, { team, slug }) {
commit(types.TEAM_USER_REQUEST, { team, slug })
users.getOne(team, slug)
.then(data => commit(types.TEAM_USER_SUCCESS, { team, slug, data }))
.catch(error => commit(types.TEAM_USER_FAILURE, { team, slug, error }))
}
}
// mutations
const mutations = {
[types.TEAM_USER_REQUEST] (state, { team, slug }) {
state.users = {
...state.users,
[team]: {
...(state.users[team] || {}),
[slug]: {
loading: true,
error: null,
slug
}
}
}
},
[types.TEAM_USER_SUCCESS] (state, { team, slug, data }) {
state.users = {
...state.users,
[team]: {
...(state.users[team] || {}),
[slug]: {
...data,
slug,
loading: false
}
}
}
},
[types.TEAM_USER_FAILURE] (state, { team, slug, error }) {
state.users = {
...state.users,
[team]: {
...(state.users[team] || {}),
[slug]: {
slug,
loading: false,
error
}
}
}
}
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}
You imagine that a team does not only have users, I have many other models of that type, and I should link them together. This method works, but I find it rather cumbersome to put in place (especially that it is a simple get, I will have plenty of other actions of this kind). Would you have any advice on my architecture?
Thank you!
I've found that the best way to keep the Vuex store flexible is to normalize it and keep your data items as flat as possible. That means storing all of your users in one structure and finding a way to uniquely identify them.
What if we combine the team and user slug to create a unique identifier? Here's how I imagine your users with a red team and a blue team:
const state = {
users: {
allTeamSlugs: [
'blue1',
'blue2',
'blue3',
'red1',
'red2',
// etc...
],
byTeamSlug: {
blue1: {
slug: 1,
team: 'blue',
teamSlug: 'blue1'
},
// blue2, blue3, etc...
red1: {
slug: 1,
team: 'red',
teamSlug: 'red1'
},
// red2, etc...
}
}
}
And the teamSlug property doesn't need to exist for each user in your API. You can create it in your mutation as you load data into the store.
const mutations = {
[types.TEAM_USER_SUCCESS] (state, { team, slug, data }) {
const teamSlug = [team, slug].join('')
state.users.byTeamSlug = {
...state.users.byTeamSlug,
[teamSlug]: {
...data,
slug: slug,
team: team,
teamSlug: teamSlug
}
}
state.users.allTeamSlugs = [
...new Set([ // The Set ensures uniqueness
...state.users.allTeamSlugs,
teamSlug
])
]
},
// ...
}
Then your getters might work like this:
const getters = {
allUsers: state => {
return state.users.allTeamSlugs.map((teamSlug) => {
return state.users.byTeamSlug[teamSlug];
});
},
usersByTeam: (state, getters) => (inputTeam) => {
return getters.allUsers.filter((user) => user.team === inputTeam);
},
getOne: state => (team, slug) => { // or maybe "userByTeamSlug"?
const teamSlug = [team, slug].join('');
return state.users.byTeamSlug[teamSlug]; // or undefined if none found
}
}
Redux has a great article about normalization that I always find myself coming back to: https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape#designing-a-normalized-state
Related
I am only using RTK to make API calls. Not using for any state management stuff like using Slices or Redux Thunk etc. I don't have a clear understanding of those yet... (just a full disclosure)
I have tried this:
export const gymApi = createApi({
reducerPath: 'gymApi',
baseQuery: fetchBaseQuery({ baseUrl }),
endpoints: (builder) => ({
getAllWorkouts: builder.query({
query: () => makeApiCall(`/exercises`),
}),
getWorkoutByBodyPart: builder.query({
query: (bodyPart) => {
console.log('BodyPart in GymApi:', bodyPart);
if (bodyPart !== 'all') {
return makeApiCall(`/exercises/bodyPart/${bodyPart}`);
} else {
return null;
}
},
})
});
And tried this:
getWorkoutByBodyPart: builder.query({
query: (bodyPart) => {
console.log('BodyPart in GymApi:', bodyPart);
if (bodyPart !== 'all') {
return makeApiCall(`/exercises/bodyPart/${bodyPart}`);
} else {
return Promise.resolve({ data: [] }); // or Promise.resolve({});
}
},
})
with no luck. I'm calling the hooks from the home page like this:
const Exercises = ({ exercises, setExercises, bodyPart }) => {
// THE FOLLOWING WORKS. ONLY USGING STATIC DATA DUE TO API HARD LIMITS
const { data: exercisesData, isFetching: isFetchingAllWorkouts } =
useGetAllWorkoutsQuery();
const { data: exercisesDataByCategory, isFetching: isFetchingByCategory } =
useGetWorkoutByBodyPartQuery(bodyPart);
useEffect(() => {
if (exercisesData && exercisesDataByCategory) {
if (bodyPart === 'all') {
setExercises(exercisesData);
} else {
setExercises(exercisesDataByCategory);
}
}
}, [exercisesData, bodyPart, isFetchingAllWorkouts, isFetchingByCategory]);
It is working but with every refresh, I get a Warning: "Category not found ..." from the API ... basically, there is no endpoint called "all" in the ExerciseDB API (in RapidAPIs). So every time an "all" is passed as a Category, it gives me a 401. Now the App works fine. I was just wondering if there is a cleaner way to do this. I mean, I don't wanna make a call to the API when the Category is "all".
I must say that I'm new to this. Trying to get out of using Fetch all the time and take advantage of RTK caching. Any help in the correct direction will be highly appreciated. Thanx in advance.
You can either pass {skip: true} as an option to the query hook, or import the special skipToken value from RTK and pass that as the query argument.
See the RTK docs for more details:
https://redux-toolkit.js.org/rtk-query/usage/conditional-fetching
so I am trying to 'chain' togther multiple queries that are somewhat dependent on each other, using RTK Query and I'm not getting very far...
APIS
import { baseApi } from '#/lib/rtkQuery/baseApi';
export const personContactApi = baseApi
.enhanceEndpoints({ addTagTypes: ['mail_packs'] })
.injectEndpoints({
endpoints: (build) => ({
createList: build.mutation({
query: (body) => {
return {
url: `/person/list/`,
method: 'POST',
body,
};
},
}),
addPersonsToList: build.mutation({
query: ({ ListId, personArray }) => {
return {
url: `/person/list/${ListId}/add-persons/`,
method: 'POST',
body: { persons: personArray },
};
},
}),
sendList: build.mutation({
query: ({ ListId }) => {
return {
url: `/person/list/${ListId}/submit/`,
method: 'POST',
};
},
}),
}),
});
export const { useCreateListMutation, useAddpersonsToListMutation, useSendListMutation } =
personContactApi;
Query functions
const [createList, { data: listResponseObject, isSuccess: createListSuccess, isError: createListError }] = useCreateListMutation();
const [addPersonsToListMutation, { isSuccess: addPersonsToListSuccess, isError: addPersonsToListError }] = useAddPersonsToListMutation();
const [sendList, { isSuccess: sendListSuccess, isError: sendListError }] = useSendListMutation();
useEffect
useEffect(() => {
// When list successfully created, add persons to list
if (createListSuccess) {
addPersonsToListMutation({
ListId: listResponseObject?.id,
personsArray: selectedPersons,
});
}
}, [
addPersonsToListMutation,
createListSuccess,
listResponseObject,
selectedPersons,
]);
useEffect(() => {
// When persons have been successfully added to mailing list, send mailing list
if (addPersonsToListSuccess) {
sendList({
listId: listResponseObject?.id,
});
}
}, [
addPersonsToListSuccess,
listResponseObject,
sendList,
]);
These are the 3 queries / mutations and they need to go in order, once the create query is success we fire the add, once that is a success we fire the send
The add and send queries are also dependent on an id returned in the response from the createList query, and the add query required an array of ids representing the person objects being added to the list
I've hacked togther a solution using multiple useEffects but it is very brittle and obviously not the ideal way to handle this situation, any one have a better way I'm all ears.
One way of solving this would be to follow one of their official suggestions:
If you need to access the error or success payload immediately after a mutation, you can chain .unwrap().
In your example, the code would be something like this
createList(input_object_goes_here)
.unwrap()
.then(createListResponse => {
addPersonsToListMutation(createListResponse.values_you_need)
.unwrap()
.then(addPersonsResponse => sendList({ listId: addPersonsResponse.id }))
})
Hope I got the syntax right, but you should get the idea.
Source: https://redux-toolkit.js.org/rtk-query/usage/error-handling
I have a problem with my code. I currently have some data like the one below;
users: [
{
name: 'bolu',
features: ['Tall'],
},
{
name: 'cam',
features: ['Bearded', 'Short'],
},
],
};
What I am trying to do is delete/remove a single feature - for example if I pass in 'short' into my redux action. I'd like for it (the 'Short' text) to be removed from the features array. I currently have my redux action set up this way:
export interface UsersDataState {
name: string,
features: Array<string>,
}
export interface UsersState {
users: UsersDataState[];
}
const initialState: UsersState = {
users: [],
};
export const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {
removeUser: (state, action: PayloadAction<string>) => {
const removedUsers = state.users.filter((user) => user.features.indexOf(action.payload));
state.users = removedUsers;
},
},
});
So here I am passing in the value in (action.payload is the value being passed in). When this action is dispatched, I want to remove just the word that is passed in from the features array. I hope this is clearer now.
This doesn't work for some reason and I am unable to figure out why. Any help would be appreciated please, thank you.
Your code doesn't match your state structure. Replace traits with users, and values with features.
It looks like that's a part of a reducer, not an action (which is an object, not a function).
You should be returning a new state from the reducer.
Given your update the function should be called removeFeature.
So, I've corrected a few bits of your code based on what I remember from Redux. Note: contrived example.
// State
const state={users:[{name:"joe",features:["Mean","Short"]},{name:"bolu",features:["Tall"]},{name:"cam",features:["Bearded","Short"]}]};
// The reducer accepts a state, and an action
function reducer(state, action) {
// We destructure the type, and payload, from the action object
const { type, payload } = action;
// Depending on the type...
switch (type) {
case 'removeFeature': {
// `map` over the users (we return a new state array)
return state.users.map(user => {
// `filter` out the feature elements
// that match the payload
const updatedFeatures = user.features.filter(feature => {
return feature !== payload;
});
// Return a new updated object
return { ...user, features: updatedFeatures };
});
}
default: return state;
}
}
const updatedState = reducer(state, {
type: 'removeFeature',
payload: 'Short'
});
console.log(updatedState);
I have written a custom merge function for the field products on type Session. It seems the merge function is only being called when I initialise the object Session:1 with its products, and not when I update products later using cache.modify.
My merge function:
const client = new ApolloClient({
uri: 'http://localhost:8081/graphql',
cache: new InMemoryCache({
typePolicies: {
Session: {
fields: {
products: {
merge (existing, incoming) {
// this is only being called on useQuery(HydrateSession), not useMutation(UpsertProduct)
console.log('existing', JSON.stringify(existing, null, 2))
console.log('incoming', JSON.stringify(incoming, null, 2))
// remove duplicates when latestProduct has the same id as an existing product — [..., latestProduct]
if (incoming.filter(p => p.id === incoming[incoming.length - 1].id).length > 1) return existing
return incoming
}
}
}
}
}
})
})
Initialisation of Session:
const HydrateSession = gql`
query {
session {
id
products {
id
}
}
}
`
...
useQuery(HydrateSession)
Updating products later using cache.modify:
const UpsertProduct = gql`
mutation UpsertProduct($product: ProductInput!) {
upsertProduct(product: $product) {
id
}
}
`
...
const [upsertProductMutation] = useMutation(UpsertProduct)
const onClick = async () => {
await upsertProductMutation({
variables: {
product: {
id: 2
}
},
update: (cache, mutationResult) => {
cache.modify({
id: 'Session:1',
fields: {
products: previous => [...previous, mutationResult.data.upsertProduct]
}
})
}
})
}
I have a full working example here https://github.com/jsindos/apollo-play, run npm i and then start two separate processes with npm start and npm serve. After clicking the button triggering the mutation, the merge function is not run (as seen by the absence of console.log statements in the console).
modify circumvents any merge functions you've defined, which means that fields are always overwritten with exactly the values you specify.
https://www.apollographql.com/docs/react/caching/cache-interaction/#using-cachemodify
Reading documentation is a good thing.
I am trying to get camera roll photos using react native CameraRoll.getPhotos API. The issue I found that the documentation is not great. In react-native official documentation there are two terms that were mentioned getPhotosReturnChecker and getPhotosParamChecker where I can get the detail about this parameters.
I found the following object that can be passed to CameraRoll.getPhotos from bhwgroup blog
{
first: ..., // (required) The number of photos wanted in reverse order of the photo application
after: ..., // A cursor returned from a previous call to 'getPhotos'
groupTypes: ..., // Specifies which group types to filter the results to
// One of ['Album', 'All', 'Event', 'Faces', 'Library', 'PhotoStream', 'SavedPhotos'(default)]
groupName: ..., // Specifies filter on group names, like 'Recent Photos' or custom album titles
assetType: ... // Specifies filter on assetType
// One of ['All', 'Videos', 'Photos'(default)]
}
According to these it always require a parameter first which dictates how many pictures we can get from CameraRoll. Instead if I want all the photos from camera roll how can I get it?
You'll want to do some paging to access all photos. Basically, you are loading them in chunks, and keeping track of the place where you left off after each fetch. You'll want a state similar to this:
this.state = {
dataSource: ds.cloneWithRows([]),
assets: [],
lastCursor: null,
noMorePhotos: false,
loadingMore: false,
};
Then fetching functions similar to these. This example assumes you are using a ListView to display your photos using a ListView.DataSource
tryPhotoLoad() {
if (!this.state.loadingMore) {
this.setState({ loadingMore: true }, () => { this.loadPhotos(); });
}
}
loadPhotos() {
const fetchParams = {
first: 35,
groupTypes: 'SavedPhotos',
assetType: 'Photos',
};
if (Platform.OS === 'android') {
// not supported in android
delete fetchParams.groupTypes;
}
if (this.state.lastCursor) {
fetchParams.after = this.state.lastCursor;
}
CameraRoll.getPhotos(fetchParams).then((data) => {
this.appendAssets(data);
}).catch((e) => {
console.log(e);
});
}
appendAssets(data) {
const assets = data.edges;
const nextState = {
loadingMore: false,
};
if (!data.page_info.has_next_page) {
nextState.noMorePhotos = true;
}
if (assets.length > 0) {
nextState.lastCursor = data.page_info.end_cursor;
nextState.assets = this.state.assets.concat(assets);
nextState.dataSource = this.state.dataSource.cloneWithRows(
_.chunk(nextState.assets, 3)
);
}
this.setState(nextState);
}
endReached() {
if (!this.state.noMorePhotos) {
this.tryPhotoLoad();
}
}