How to handle an entitiy relation without bidirectional links in normalizr - javascript

I'm adding normalizr to a redux application and I'm having a tricky time handling a unidirectional link in the data returned from an API.
Two separate API calls are made, the first one returns an author:
author = {
...authorFields,
}
The second one returns a list of books:
books = [
{
...bookFields,
authorId,
},
...
]
Here is my normalizr schema:
const book = new schema.Entity('books')
const author = new schema.Entity('authors', {
books: [ book ],
})
book.define('book', {
author,
})
In our API response, we only have a link in the book, and not contained in the author. Just calling normalize() on the case won't give us the nice normalized state we want.
My target state is this (the normalized state that we would get if the author response from the API contained the books as nested entities):
{
authors: {
"101": {
...authorFields
books: [
"1001",
"1002",
etc...
]
}
books {
"1001" : {
"1001": {
...bookFields,
author: "101",
},
"1002": {
...bookFields,
author: "101",
}
}
}
}
My approach was to handle things in the reducer.
Here's the books action creator (I'm using thunk):
function fetchBooksForAuthor(authorId) {
return (dispatch, _, schema) => {
return getBooksFromAPIByAuthor(authorId)
.then(books => {
const normalizedData = normalize(books, [ schema.book ])
dispatch(addBooks({
authorId,
...normalizedData
}))
})
}
}
Here are the reducers:
function books(state = {}, action) {
switch (action.type) {
case ADD_BOOKS:
return {
...state,
...action.payload.entities.books
}
...
}
}
function authors(state = {}, action) {
switch (action.type) {
case ADD_BOOKS:
const authorId = action.payload.authorId
const author = state[authorId]
return {
...state,
[authorId]: {
...author,
...action.payload.result
}
}
...
}
}
Would this be the best way to handle this type of case?
Is there any way of handling this type of case entirely inside normalizr? If not, does anyone have any suggestions as to the best way to handle this? Add another link in the API ^^?

Related

Maintaining react state with a hierarchical object using react hooks (add or update)

I have an a state object in React that looks something like this (book/chapter/section/item):
const book = {
id: "123",
name: "book1",
chapters: [
{
id: "123",
name: "chapter1",
sections: [
{
id: "4r4",
name: "section1",
items: [
{
id: "443",
name: "some item"
}
]
}
]
},
{
id: "222",
name: "chapter2",
sections: []
}
]
}
I have code that adds or inserts a new chapter object that is working. I am using:
// for creating a new chapter:
setSelectedBook(old => {
return {
...old,
chapters: [
...old.chapters,
newChapter // insert new object
]
}
})
And for the chapter update, this is working:
setSelectedBook(old => {
return {
...old,
chapters: [
...old.chapters.map(ch => {
return ch.id === selectedChapterId
? {...ch, name: selectedChapter.name}
: ch
})
]
}
})
But for my update/create for the sections, I'm having trouble using the same approach. I'm getting syntax errors trying to access the sections from book.chapters. For example, with the add I need:
// for creating a new section:
setSelectedBook(old => {
return {
...old,
chapters: [
...old.chapters,
...old.chapters.sections?
newSection // how to copy chapters and the sections and insert a new one?
]
}
})
I know with React you're supposed to return all the previous state except for what you're changing. Would a reducer make a difference or not really?
I should note, I have 4 simple lists in my ui. A list of books/chapters/sections/items, and on any given operation I'm only adding/updating a particular level/object at a time and sending that object to the backend api on each save. So it's books for list 1 and selectedBook.chapters for list 2, and selectedChapter.sections for list 3 and selectedSection.items for list 4.
But I need to display the new state when done saving. I thought I could do that with one bookState object and a selectedThing state for whatever you're working on.
Hopefully that makes sense. I haven't had to do this before. Thanks for any guidance.
for adding new Section
setSelectedBook( book =>{
let selectedChapter = book.chapters.find(ch => ch.id === selectedChapterId )
selectedChapter.sections=[...selectedChapter.sections, newSection ]
return {...book}
})
For updating a section's name
setSelectedBook(book=>{
let selectedChapter = book.chapters.find(ch => ch.id === selectedChapterId )
let selectedSection = selectedChapter.sections.find(sec => sec.id === selectedSectionId )
selectedSection.name = newName
return {...book}
})
For updating item's name
setSelectedBook(book =>{
let selectedChapter = book.chapters.find(ch => ch.id === selectedChapterId )
let selectedSection = selectedChapter.sections.find(sec => sec.id === selectedSectionId )
let selectedItem = selectedSection.items.find(itm => itm.id === selectedItemId)
selectedItem.name = newItemName
return {...book}
})
I hope you can see the pattern.
I think the map should work for this use case, like in your example.
setSelectedBook(old => {
return {
...old,
chapters: [
...old.chapters.map(ch => {
return { ...ch, sections: [...ch.sections, newSection] }
})
]
}
})
In your last code block you are trying to put chapters, sections and the new section into the same array at the same level, not inside each other.
Updating deep nested state objects in React is always difficult. Without knowing all the details of your implementation, it's hard to say how to optimize, but you should think hard about different ways you can store that state in a flatter way. Sometimes it is not possible, and in those cases, there are libraries like Immer that can help that you can look in to.
Using the state object you provided in the question, perhaps you can make all of those arrays into objects with id for keys:
const book = {
id: "123",
name: "book1",
chapters: {
"123": {
id: "123",
name: "chapter1",
sections: {
"4r4": {
id: "4r4",
name: "section1",
items: {
"443": {
id: "443",
name: "some item"
}
}
}
}
},
"222": {
id: "222",
name: "chapter2",
sections: {},
}
]
}
With this, you no longer need to use map or find when setting state.
// for creating a new chapter:
setSelectedBook(old => {
return {
...old,
chapters: {
...old.chapters,
[newChapter.id]: newChapter
}
}
})
// for updating a chapter:
setSelectedBook(old => {
return {
...old,
chapters: {
...old.chapters,
[selectedChapter.id]: selectedChapter,
}
}
})
// for updating a section:
setSelectedBook(old => {
return {
...old,
chapters: {
...old.chapters,
[selectedChapter.id]: {
...selectedChapter,
sections: {
[selectedSectionId]: selectedSection
}
},
}
}
})
Please let me know if I misunderstood your problem.

Remove value from an array nested in an object

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.
You need to copy the objects on users and filter on features.
Here an example:
var users = [{
name: 'bolu',
features: ['Tall'],
}, {
name: 'cam',
features: ['Bearded', 'Short'],
}];
const payload = "Short";
const newUsers = users.map(user => ({ ...user,
features: user.features.filter(f => f != payload)
}));
console.log(newUsers);
Currently you are filtering users array but you should be filtering nested features array.
Try this
const removedUsers = state.users.map(user => {
return {...user, features: user.features.filter(feature => feature !== action.payload)};
})

Trying to write a recursive asynchronous search in JavaScript

I am trying to write some code that searches through a bunch of objects in a MongoDB database. I want to pull the objects from the database by ID, then those objects have ID references. The program should be searching for a specific ID through this process, first getting object from id, then ids from the object.
async function objectFinder(ID1, ID2, depth, previousList = []) {
let route = []
if (ID1 == ID2) {
return [ID2]
} else {
previousList.push(ID1)
let obj1 = await findObjectByID(ID1)
let connectedID = obj1.connections.concat(obj1.inclusions) //creates array of both references to object and references from object
let mapPromises = connectedID.map(async (id) => {
return findID(id) //async function
})
let fulfilled = await Promise.allSettled(mapPromises)
let list = fulfilled.map((object) => {
return object.value.main, object.value.included
})
list = list.filter(id => !previousList.includes(id))
for (id of list) {
await objectFinder(id, ID2, depth - 1, previousList).then(result => {
route = [ID1].concat(result)
if (route[route.length - 1] == ID2) {
return route
}})
}
}
if (route[route.length - 1] == ID2) {
return route
}
}
I am not sure how to make it so that my code works like a tree search, with each object and ID being a node.
I didn't look too much into your code as I strongly believe in letting your database do the work for you if possible.
In this case Mongo has the $graphLookup aggregation stage, which allows recursive lookups. here is a quick example on how to use it:
db.collection.aggregate([
{
$match: {
_id: 1,
}
},
{
"$graphLookup": {
"from": "collection",
"startWith": "$inclusions",
"connectFromField": "inclusions",
"connectToField": "_id",
"as": "matches",
}
},
{
//the rest of the pipeline is just to restore the original structure you don't need this
$addFields: {
matches: {
"$concatArrays": [
[
{
_id: "$_id",
inclusions: "$inclusions"
}
],
"$matches"
]
}
}
},
{
$unwind: "$matches"
},
{
"$replaceRoot": {
"newRoot": "$matches"
}
}
])
Mongo Playground
If for whatever reason you want to keep this in code then I would take a look at your for loop:
for (id of list) {
await objectFinder(id, ID2, depth - 1, previousList).then(result => {
route = [ID1].concat(result);
if (route[route.length - 1] == ID2) {
return route;
}
});
}
Just from a quick glance I can tell you're executing this:
route = [ID1].concat(result);
Many times at the same level. Additional I could not understand your bottom return statements, I feel like there might be an issue there.

React redux - updating nested array in state

I'm trying some app in react redux and i have a problem with updating (push, remove, update) the nested array in state.
I have some object called service like this:
{
name: 'xzy',
properties: [
{ id: 1, sName: 'xxx'},
{ id: 2, sName: 'zzz'},
]
}
Whatever I did (in case of adding property to collection) in the reducer with the properties collection generate problem that all properties got same values as the last I had recently added -> Added property object is in service properties collection but the action replace all values in all properties in this collection.
My reducer:
export function service(state = {}, action) {
switch (action.type) {
case 'ADD_NEW_PROPERTY':
console.log(action.property) // correct new property
const service = {
...state, properties: [
...state.properties, action.property
]
}
console.log(service); // new property is pushed in collection but all properties get same values
return service
default:
return state;
}
}
I have tried some solution with immutability-helper library and it generate the same problem:
export function service(state = {}, action) {
case 'ADD_NEW_PROPERTY':
return update(state, {properties: {$push: [action.property]}})
default:
return state;
}
For example when I add new property { id: 1, sName: 'NEW'} to example above I will get this state:
{
name: 'xzy',
properties: [
{ id: 1, sName: 'NEW'},
{ id: 1, sName: 'NEW'},
{ id: 1, sName: 'NEW'}
]
}
Can someone help? :)
Make a copy of action.property as well. Whatever is dispatching this action, it could be reusing the same object.
export function service(state = {}, action) {
switch (action.type) {
case 'ADD_NEW_PROPERTY':
console.log(action.property) // correct new property
const service = {
...state,
properties: [
...state.properties,
{ ...action.property }
]
}
console.log(service); // new property is pushed in collection but all properties get same values
return service
default:
return state;
}
}
I'd recommend you to use Immutable data https://facebook.github.io/immutable-js/docs/#/List
import { fromJS, List } from 'immutable';
const initialState = fromJS({
propeties: List([{ id: 1, sName: 'xyz' }]
}
function reducer(state = initialState, action) {
case ADD_NEW_PROPERTY:
return state
.update('properties', list => list.push(action.property));
// ...
}
Your service reducer should probably look somewhat like this:
// Copy the state, because we're not allowed to overwrite the original argument
const service = { ...state };
service.properties.append(action.property)
return service
You should always copy the state before returning it.
export default function(state = {}, action) {
switch(action.type) {
case 'GET_DATA_RECEIVE_COMPLETE': {
const data = action.firebaseData;
const newState = Object.assign({}, state, {
data
});
return newState
}
default:
return state;
}
}

Updating object inside redux state throws an error

I'm trying to update array inside object in my reducer.
const initialState = {
group: {
name: "",
date: "",
description: "",
users: [],
posts: []
},
morePosts: false,
groups: []
};
export function groups(state = initialState, action) {
switch (action.type) {
.......
case REQUEST_MORE_POSTS:
{
return {
...state,
group:{
...state.group,
posts: [
...state.group.posts,
...action.payload.posts
]
},
morePosts: action.payload.morePosts
}
}
case ADD_NEW_POST:
{
return {
...state,
group:{
...state.group,
posts: [
action.payload,
...state.group.posts
]
}
}
}
........
default:
return state;
}
}
Unfortunately in both cases I get an error:
It works when I extract posts out of my group object but I need it inside.
I can't figure out what I've done wrong here. Can someone point me to the right direction?
Here is an action creator for adding new post.
export function addPost(url, payload) {
return function(dispatch) {
axios.post(url + "php/addPostGroup.php", {payload}).then(response => {
dispatch({
type: ADD_NEW_POST,
payload: response.data.post
})
})
}
}
response.data.post is a simple object.
I've added console.log() before dispatch. This is how my response looks like:
Alright I solved it. Before fetching any data my state.group.posts array was for some reason treated as undefined. I had to manually declare it as empty array using
var posts = state.group.posts != undefined ? state.group.posts:[];

Categories