Mobx-state-tree: Question: Normalizing data structure

Created on 7 Jul 2017  路  4Comments  路  Source: mobxjs/mobx-state-tree

I want to have a normalized db store, including relationships.

{
  books: {
    'b1': {
      id: 'b1', title: 'book 1',
      author /* or authorId: */: 'a1',
      categories /* or categoryIds: */: ['c1', 'c2'],
    },
  },
  authors: {
    'a1': { id: 'a1', name: 'author 1' },
  },
  categories: {
    'c1': { id: 'c1', name: 'c1' },
    'c2': { id: 'c2', name: 'c2' },
  },
}

This is straightforward to build, but inserting the data may become complex.

If the server I'm talking to returns:

[
  {
    id: 'b1', title: 'b1',
    author: { id: 'a1', name: 'a1' },
    categories: [ { id: 'c1', name: 'c1' }, { id: 'c2', name: 'c2' } ]
  },
  {
    id: 'b2', title: 'b2',
    author: { id: 'a2', name: 'a2' },
    categories: [ { id: 'c1', name: 'c1' }, { id: 'c3', name: 'c3' } ]
  },
  ...
]

My Book model has a author: types.reference(Author) and categories: types.array(Category), just inserting a Book into a collection doesn't insert author and categories to the right places in the tree.

Ideally I could throw entities in some structure at the store and it'll upsert them by type and ID, walk through the reference properties and do that recursively.

Is there anything like that in MST, or anything planned?

Most helpful comment

Yay, I just wrote a demo, not simple.

import React from "react"
import { types, getSnapshot } from "mobx-state-tree"
import { observer } from "mobx-react"
import { inspect, render } from "mobx-state-tree-playground"

const Author = types.model('Author', {
    id: types.identifier(),
    name: types.string
})

const Category = types.model('Category', {
    id: types.identifier(),
    name: types.string
})

const Book = types.model('Book', {
    id: types.identifier(),
    title: types.string,
    author: types.reference(Author),
    categories: types.array(types.reference(Category))
})

const AppModel = types.model({
    books: types.map(Book),
    authors: types.map(Author),
    categories: types.map(Category)
}, {
    addAuthor(authorData) {
        const author = Author.create(authorData)
        this.authors.put(author)
    },
    addBook(bookData) {
        const book = Book.create(bookData)
        this.books.put(book)
    },
    addCategory(categoryData) {
        const category = Category.create(categoryData)
        this.categories.put(category)
    }
})

const store = AppModel.create({books: {}, authors: {}, categories: {}})
inspect(store)

//////////////////////// Convert function
function convertStore(store, data) {
    const AuthorData = types.model('Author', {
        id: types.identifier(),
        name: types.string
    }, {
        afterAttach() {
            store.addAuthor(getSnapshot(this))
        }
    })

    const CategoryData = types.model('Category', {
        id: types.identifier(),
        name: types.string
    }, {
        afterAttach() {
            store.addCategory(getSnapshot(this))
        }
    })

    const BookData = types.model('Book', {
        id: types.identifier(),
        title: types.string,
        author: AuthorData,
        categories: types.array(CategoryData)
    }, {
        afterAttach() {
            const bookData = getSnapshot(this)
            store.addBook(Object.assign({}, bookData, {
                author: this.author.id,
                categories: this.categories.map(c => c.id)
            }))
        }
    })

    const DataStore = types.model('DataStore', {
        books: types.array(BookData)
    })

    DataStore.create({books: data})
}

//////////////////// Test
const data = [
  {
    id: 'b1', title: 'b1',
    author: { id: 'a1', name: 'a1' },
    categories: [ { id: 'c1', name: 'c1' }, { id: 'c2', name: 'c2' } ]
  },
  {
    id: 'b2', title: 'b2',
    author: { id: 'a2', name: 'a2' },
    categories: [ { id: 'c1', name: 'c1' }, { id: 'c3', name: 'c3' } ]
  }
]

convertStore(store, data)

See in Playground

All 4 comments

Yay, I just wrote a demo, not simple.

import React from "react"
import { types, getSnapshot } from "mobx-state-tree"
import { observer } from "mobx-react"
import { inspect, render } from "mobx-state-tree-playground"

const Author = types.model('Author', {
    id: types.identifier(),
    name: types.string
})

const Category = types.model('Category', {
    id: types.identifier(),
    name: types.string
})

const Book = types.model('Book', {
    id: types.identifier(),
    title: types.string,
    author: types.reference(Author),
    categories: types.array(types.reference(Category))
})

const AppModel = types.model({
    books: types.map(Book),
    authors: types.map(Author),
    categories: types.map(Category)
}, {
    addAuthor(authorData) {
        const author = Author.create(authorData)
        this.authors.put(author)
    },
    addBook(bookData) {
        const book = Book.create(bookData)
        this.books.put(book)
    },
    addCategory(categoryData) {
        const category = Category.create(categoryData)
        this.categories.put(category)
    }
})

const store = AppModel.create({books: {}, authors: {}, categories: {}})
inspect(store)

//////////////////////// Convert function
function convertStore(store, data) {
    const AuthorData = types.model('Author', {
        id: types.identifier(),
        name: types.string
    }, {
        afterAttach() {
            store.addAuthor(getSnapshot(this))
        }
    })

    const CategoryData = types.model('Category', {
        id: types.identifier(),
        name: types.string
    }, {
        afterAttach() {
            store.addCategory(getSnapshot(this))
        }
    })

    const BookData = types.model('Book', {
        id: types.identifier(),
        title: types.string,
        author: AuthorData,
        categories: types.array(CategoryData)
    }, {
        afterAttach() {
            const bookData = getSnapshot(this)
            store.addBook(Object.assign({}, bookData, {
                author: this.author.id,
                categories: this.categories.map(c => c.id)
            }))
        }
    })

    const DataStore = types.model('DataStore', {
        books: types.array(BookData)
    })

    DataStore.create({books: data})
}

//////////////////// Test
const data = [
  {
    id: 'b1', title: 'b1',
    author: { id: 'a1', name: 'a1' },
    categories: [ { id: 'c1', name: 'c1' }, { id: 'c2', name: 'c2' } ]
  },
  {
    id: 'b2', title: 'b2',
    author: { id: 'a2', name: 'a2' },
    categories: [ { id: 'c1', name: 'c1' }, { id: 'c3', name: 'c3' } ]
  }
]

convertStore(store, data)

See in Playground

There is no included solution yet, but for a low boilerplate solution you can check out normalizr and use it :)

    addFetchedData(data){
      const author = new schema.Entity('authors')
      const category = new schema.Entity('categories')
      const book = new schema.Entity('books', {
        author,
        categories: [category]
      })

      const {entities} = normalize(data, [book])
      this.authors.merge(entities.authors)
      this.categories.merge(entities.categories)
      this.books.merge(entities.books)
    }

https://codesandbox.io/s/X64kr4QRm

Wow! Nearly perfect for me!

Thanks!

Thank you so much! normalizr is indeed where I was aiming. Would be interesting to write a wrapper that translates MST models to normalizr, so all of that is seamless. I'll explore this direction.
Closing for now :)

Was this page helpful?
0 / 5 - 0 ratings