Redux - Normalized nested data organization inside createSlice - redux

I have a deeply nested data object that comes back from my API which looks like the JSON below.
I am using Redux toolkit's createSlice to create a slice of a trip
So currently in my createSlice, I want to store an array of trips.
I also want the ability to update a single trip or part of the trip
for example, let's say I want to update a trip item's start date
or, let's say I want to update a trip item's member's name
My questions and concerns:
I currently have all of these entities coming back into the trip createSlice but I am not sure if, once the entities are normalized, should they be separated into separate createSlices? if so, how is this done or is this an anti pattern?
how should nested entities be defined in initialState?
should I define all of my normalized entities in my initalState?
if I do that, how would my reducers look like when I want to update a trip_item or trip_item_member ?
does my normalized data even look "correct"? I have omitted using mergeStrategy between trips_items_members and trip_members which I know I should do but haven't figured out how that works yet or if it's necessary here?
Note:
There is an example in the RTK docs here which shows createSlice being used with 3 separate entities, which originally came from 1 API call. It looks like 3 separate files however it is unclear how data is shared amongst them.
This is how my trip createSlice looks like
/**
* Get trip by ID action
*/
export const getTripByID = createAsyncThunk(
'trips/getTripByID',
async ({ uid }) => {
const response = await findOne(uid)
const normalized = normalize(response, trip)
return normalized.entities
},
)
const tripsAdapter = createEntityAdapter({
selectId: entity => entity.trip_id,
sortComparer: (a, b) => b.start_date.localeCompare(a.start_date),
loading: '',
error: '',
data: [],
})
export const {
selectById: selectTripById,
selectIds: selectTripIds,
selectEntities: selectTripEntities,
selectAll: selectAllTrips,
selectTotal: selectTotalTrips,
} = tripsAdapter.getSelectors(state => state.trip)
const initialState = tripsAdapter.getInitialState()
const tripSlice = createSlice({
name: 'trips',
initialState,
extraReducers: builder => {
builder.addCase(getAllTrips.fulfilled, (state, { payload }) => {
tripsAdapter.upsertMany(state, payload)
state.loading = false
})
builder.addCase(getTripByID.fulfilled, (state, { payload }) => {
console.log('payload', payload)
tripsAdapter.upsertMany(state, payload)
state.loading = false
})
},
})
export default tripSlice.reducer
API response that comes back from await findOne(uid)
{
created_by: "6040c2d1-ea57-43b6-b5f2-58e84b220f4e",
deleted_by: null,
destination: "Valencia",
end_date: "2020-10-04",
start_date: "2020-09-27",
trip_id: "34a620e8-51ff-4572-b466-a950a8ce1c8a",
uid: "14047a5b-2fe5-46c9-b7f2-e9b5d14db05b",
updated_by: null,
trip_items: [
{
destination: "Mezzanine Level Shivaji Stadium Metro Station, Baba Kharak Singh Rd, Hanuman Road Area, Connaught Place, New Delhi, Delhi 110001, India",
end_date: "2020-09-28",
end_time: "2020-09-28T01:20:15.906Z",
note: null,
start_date: "2020-09-28",
start_time: "2020-09-28T01:20:15.906Z",
trip_item_id: "bd775be7-2129-42c0-a231-5a568b0f565d",
trips_items_members: [
{
trip_item_member_id: "76b54a80-4d09-4768-bc5a-4d7e153e66dc",
uid: "4b88f9af-8639-4bb0-93fa-96fe97e03d02",
}
],
uid: "e5f81a6d-1a0d-4456-9d4e-579e80bc27d8",
}
],
trips_members: [
{
trip_member_id: "76b54a80-4d09-4768-bc5a-4d7e153e66dc",
uid: "4b88f9af-8639-4bb0-93fa-96fe97e03d02",
role: "ADMIN"
}
]
}
This is my normalizr schema
const tripItemMember = new schema.Entity(
'trips_items_members',
{},
{ idAttribute: 'trip_item_member_id' },
)
const tripItem = new schema.Entity(
'trips_items',
{
trips_items_members: [tripItemMember],
},
{
idAttribute: 'trip_item_id',
},
)
const tripMember = new schema.Entity(
'trips_members',
{},
{
idAttribute: 'trip_member_id',
},
)
export const trip = new schema.Entity(
'trip',
{
trips_items: [tripItem],
trips_members: [tripMember],
},
{
idAttribute: 'trip_id',
},
)
This is the output from normalizr
trip: {
"34a620e8-51ff-4572-b466-a950a8ce1c8a": {
created_by: "6040c2d1-ea57-43b6-b5f2-58e84b220f4e"
deleted_by: null
destination: "Valencia"
end_date: "2020-10-04"
start_date: "2020-09-27"
trip_id: "34a620e8-51ff-4572-b466-a950a8ce1c8a"
trips_items: ["bd775be7-2129-42c0-a231-5a568b0f565d"]
trips_members: ["76b54a80-4d09-4768-bc5a-4d7e153e66dc"]
uid: "14047a5b-2fe5-46c9-b7f2-e9b5d14db05b"
updated_by: null
}
}
trips_items:{
"0a56da0f-f13b-4c3d-896d-30bccbe48a5a": {
destination: "Mezzanine Level Shivaji Stadium Metro Station"
end_date: "2020-09-28"
end_time: "2020-09-28T01:20:15.906Z"
note: null
start_date: "2020-09-28"
start_time: "2020-09-28T01:20:15.906Z"
trip_item_id: "0a56da0f-f13b-4c3d-896d-30bccbe48a5a"
trips_items_members: []
uid: "25d20a9d-1eb9-4226-926d-4d743aa9d5dc"
}
}
trips_members: {
"76b54a80-4d09-4768-bc5a-4d7e153e66dc": {
role: "ADMIN"
trip_member_id: "76b54a80-4d09-4768-bc5a-4d7e153e66dc"
uid: "4b88f9af-8639-4bb0-93fa-96fe97e03d02"
}
}

Your setup is very much like this detailed example from the redux-toolkit docs. They are fetching articles, but each article comes with embedded users and comments. They define separate slices for each of the three entities.
The comments slice has no actions or reducers of its own, but it uses the extraReducers property to respond to the article received action and store the embedded comments.
const commentsAdapter = createEntityAdapter();
export const slice = createSlice({
name: "comments",
initialState: commentsAdapter.getInitialState(),
reducers: {},
extraReducers: {
[fetchArticle.fulfilled]: (state, action) => {
commentsAdapter.upsertMany(state, action.payload.comments);
}
}
});
The fetchArticle action is "owned" by the article slice, but the action payload contains entities from all three types. All slices receive all actions, so the comments and users are able to respond to this action with their own logic. Each slice doesn't have any effect on what the others can or can't do.
In your case you want to create slices for items and members. Instead of calling upsertMany(state, payload), you want the payload to be keyed by entity type so that you can call upsertMany(state, payload.members).

Related

With Strapi 4 how can I get each users music events

I'm using strapi 4 with nextjs.
In the app strapi holds music events for each user and each user should be able add and retrieve there own music events.
I am having trouble retrieving
each users music events from strapi 4
I have a custom route and custom controller
The custom route is in a file called custom-event.js and works ok it is as follows:
module.exports = {
routes: [
{
method: 'GET',
path: '/events/me',
handler: 'custom-controller.me',
config: {
me: {
auth: true,
policies: [],
middlewares: [],
}
}
},
],
}
The controller id a file called custom-controller.js and is as follows:
module.exports = createCoreController(modelUid, ({strapi }) => ({
async me(ctx) {
try {
const user = ctx.state.user;
if (!user) {
return ctx.badRequest(null, [
{messages: [{ id: 'No authorization header was found'}]}
])
}
// The line below works ok
console.log('user', user);
// The problem seems to be the line below
const data = await strapi.services.events.find({ user: user.id})
// This line does not show at all
console.log('data', data);
if (!data) {
return ctx.notFound()
}
return sanitizeEntity(data, { model: strapi.models.events })
} catch(err) {
ctx.body = err
}
}
}))
Note there are two console.logs the first console.log works it outputs the user info
The second console.log outputs the data it does not show at all. The result I get back
using insomnia is a 200 status and an empty object {}
The following line in the custom-controller.js seems to be where the problem lies it works for strapi 3 but does not seem to work for strapi 4
const data = await strapi.services.events.find({ user: user.id})
After struggling for long time, days infact, I eventually got it working. Below is the code I came up with. I found I needed two queries to the database, because I could not get the events to populate the images with one query. So I got the event ids and then used the event ids in a events query to get the events and images.
Heres the code below:
const utils = require('#strapi/utils')
const { sanitize } = utils
const { createCoreController } = require("#strapi/strapi").factories;
const modelUid = "api::event.event"
module.exports = createCoreController(modelUid, ({strapi }) => ({
async me(ctx) {
try {
const user = ctx.state.user;
if (!user) {
return ctx.badRequest(null, [
{messages: [{ id: 'No authorization header was found'}]}
])
}
// Get event ids
const events = await strapi
.db
.query('plugin::users-permissions.user')
.findMany({
where: {
id: user.id
},
populate: {
events: { select: 'id'}
}
})
if (!events) {
return ctx.notFound()
}
// Get the events into a format for the query
const newEvents = events[0].events.map(evt => ({ id: { $eq: evt.id}}))
// use the newly formatted newEvents in a query to get the users
// events and images
const eventsAndMedia = await strapi.db.query(modelUid).findMany({
where: {
$or: newEvents
},
populate: {image: true}
})
return sanitize.contentAPI.output(eventsAndMedia,
strapi.getModel(modelUid))
} catch(err) {
return ctx.internalServerError(err.message)
}
}
}))

How to generate filtered list using reselect redux based on static filtered values?

I am fetching news data from an API, in the app I need to show 3 lists. today news, yesterday news, article news.
I think I should use redux reselect. However, all the examples I am visiting has a dynamic filter value (state filter) while I need data to be fileted statically (no state changes these filters)
my state at the moment is
{news : [] }
How can I generate something like below using reselect
{news: [], todayNews:[], yesterdayNews:[], articleNews: []}
should I use reselect or I should just filter inside a component? I think reselect is memorized so I prefer to use reselect for performance
You can do something like the following:
const { createSelector } = Reselect;
const state = {
news: [
{ id: 1, name: 'one' },
{ id: 2, name: 'two' },
{ id: 3, name: 'three' },
],
};
const selectNews = (state) => state.news;
const selectOdds = createSelector(selectNews, (news) =>
news.filter(({ id }) => id % 2 !== 0)
);
const selectEvens = createSelector(selectNews, (news) =>
news.filter(({ id }) => id % 2 === 0)
);
const selectFilteredNews = createSelector(
selectNews,
selectEvens,
selectOdds,
(news, even, odd) => ({ news, even, odd })
);
const news = selectFilteredNews(state);
console.log('news:', JSON.stringify(news, undefined, 2));
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>
You use selectors when you need to calculate values based on state such as the total of a list or filtered things from a list. This way you don't need to duplicate the data in your state.

Parent Entities, nodejs

Folks,
How does one access the parent entities? Lets imagine I have 2 Kinds, users and say cars... When Kind cars is queried by plate, it returns all the entities that match the plate. How can one also get those entities' ancestor information? What if I wanted to get the parent key for the returned' entities? Is this possible?
Example where I only am able to retrieve the entities
const query = datastoreClient.createQuery('cars')
.filter('plate', '=', 'ABC');
return datastoreClient.runQuery(query).then(function (entities) {
console.log(entities);
return entities;
});
output:
[
[
{
"id": "2b49ca40-a5fb-11e7-ad5c-c95aa76161c6",
"plate": "ABC"
}
],
{
"moreResults": "NO_MORE_RESULTS",
"endCursor": "Cn0Sd2oWc35za2lsZnVsLWZyYW1lLTE4MDIxN3JdCxIFdXNlcnMiJDIxMmU2MTQwLWE1OTQtMTFlNy1iMGQ1LWUxNjBjMjA0NjI2MQwLEgRjYXJzIiQyYjQ5Y2E0MC1hNWZiLTExZTctYWQ1Yy1jOTVhYTc2MTYxYzYMGAAgAA=="
}
]
Here you have the Node.js code that does what you want:
const Datastore = require('#google-cloud/datastore');
const projectId = 'some-projectid'
// Creates a client
const datastore = new Datastore({
projectId: projectId,
});
const Query = datastore
.createQuery('cars')
.filter('plate', '=', 'some-plate-number')
datastore.runQuery(Query).then(results => {
// Task entities found.
const tasks = results[0];
console.log('Tasks:');
tasks.forEach(task => console.log(task));
Output:
Tasks: { plate: 'some-plate-number', [Symbol(KEY)]: Key {
namespace: undefined,
id: 'id-of-the-child-entity',
kind: 'cars',
parent:
Key {
namespace: undefined,
id: 'id-of-the-parent-entity',
kind: 'users',
path: [Getter] },
path: [Getter] } }

Lazy loading references from normalized Redux store

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
}
};
}

Is creating a reducer for each component that fetches data a good pattern?

Imagine you have 2 components: TasksList and OverdueTasksList. The number of tasks is big, so you cannot load all tasks with 1 network request. This means that they both need to fetch data individually.
What's the best way to organize data with Redux in this case?
Here's my idea:
{
tasks: {
t1: {
id: "t1",
name: "Task 1",
},
// ...
},
tasksList: {
isFetching: false,
error: null,
tasks: ["t1", ...]
},
overdueTasksList: {
isFetching: false,
error: null,
tasks: ["t5", ...]
}
}
This way, if you edit a task object from one component, it will be reflected in the other component as well.
What I don't like with this pattern is that you have to create a new reducer for every component that fetches data.
Could you have 'overdue' as a property of task instead? therefore you could merge the 2 reducers into one:
const initialState = [];
tasks: function(state = initialState, action){
switch(action.type){
case receiveOverduetask:
return [...state, action.overdueTask];
case receiveTask:
return [...state, action.task];
default:
return state;
}
}
You can still have the 2 separate requests, just make sure that they set the overdue property accordingly.

Resources