8000 perf(entity.get): add attribute change tracking by becomingbabyman · Pull Request #15 · homebaseio/homebase-react · GitHub
[go: up one dir, main page]

Skip to content
8000

perf(entity.get): add attribute change tracking #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/publish-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:

- run: yarn install --frozen-lockfile

- run: yarn shadow-cljs compile dev
- run: yarn shadow-cljs release dev

- name: Publish to GitHub Pages 🚀
uses: JamesIves/github-pages-deploy-action@releases/v3
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,37 @@ todos
.map(todo => todo.get('name'))
```

## Performance

Homebase React tracks the attributes consumed in each component via the `entity.get` function and scopes those attributes to their respective `useEntity` or `useQuery` hook. Re-renders are only triggered when an attribute changes.

The default caching reduces unnecessary re-renders and virtual DOM thrashing a lot. That said, it is still possible to trigger more re-renders than you might want.

One top level `useQuery` + prop drilling the entities it returns will cause all children to re-render on any change to the parent or their siblings.

To fix this we recommend passing ids to children, not whole entities. Instead get the entity in the child with `useEntity(id)`. This creates a new scope for each child so they are not affected by changes in the state of the parent or sibling components.

```js
const TodoList = () => {
const [todos] = useQuery({
$find: 'todo',
$where: { todo: { name: '$any' } }
})
return (todos.map(t => <Todo key={t.get('id')} id={t.get('id')} />))
}

// Good
const Todo = React.memo(({ id }) => {
const [todo] = useEntity(id)
// ...
})

// Bad
const Todo = React.memo(({ todo }) => {
// ...
})
```


## Docs
https://www.notion.so/Homebase-Alpha-Docs-0f0e22f3adcd4e9d87a13440ab0c7a0b
Expand Down
207 changes: 96 additions & 111 deletions js/todo-example.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ const config = {
// identity is a special unique attribute for user generated ids
// E.g. todoFilters are settings that should be easy to lookup by their identity
identity: 'todoFilters',
showCompleted: true,
project: 0
showCompleted: true
}
}, {
user: {
Expand Down Expand Up @@ -110,57 +109,6 @@ const NewTodo = () => {
)
}

const TodoFilters = () => {
const [filters] = useEntity({ identity: 'todoFilters' })
const [transact] = useTransact()
return (
<div>
<label htmlFor="show-completed">Show Completed?</label>
<input
type="checkbox"
id="show-completed"
checked={filters.get('showCompleted')}
onChange={e => transact([{ todoFilter: { id: filters.get('id'), showCompleted: e.target.checked }}])}
/>
&nbsp;·&nbsp;
<ProjectSelect
value={filters.get('project')}
onChange={project => transact([{ todoFilter: { id: filters.get('id'), project }}])}
/>
</div>
)
}

const ProjectSelect = ({ value, onChange }) => {
const [projects] = useQuery({
$find: 'project',
$where: { project: { name: '$any' } }
})
return (
<>
<label>
Project:
</label>
&nbsp;
<select
name="projects"
value={value}
onChange={e => onChange && onChange(Number(e.target.value))}
>
<option value="0"></option>
{projects.map(project => (
<option
key={project.get('id')}
value={project.get('id')}
>
{project.get('name')}
</option>
))}
</select>
</>
)
}

const TodoList = () => {
const [filters] = useEntity({ identity: 'todoFilters' })
const [todos] = useQuery({
Expand All @@ -169,36 +117,42 @@ const TodoList = () => {
})
return (
<div>
{todos
.filter(todo => {
{todos.filter(todo => {
if (!filters.get('showCompleted') && todo.get('isCompleted')) return false
if (filters.get('project') && todo.get('project', 'id') !== filters.get('project')) return false
if (filters.get('owner') && todo.get('owner', 'id') !== filters.get('owner')) return false
return true
})
.sort((a, b) => a.get('createdAt') > b.get('createdAt') ? -1 : 1)
.map(todo => <Todo key={todo.get('id')} todo={todo}/>)}
}).sort((a, b) => a.get('createdAt') > b.get('createdAt') ? -1 : 1)
.map(todo => <Todo key={todo.get('id')} id={todo.get('id')}/>)}
</div>
)
}

const Todo = ({ todo }) => (
<div>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'flex-end', paddingTop: 20}}>
<TodoCheck todo={todo} />
<TodoName todo={todo} />
</div>
// PERFORMANCE: By accepting an `id` prop instead of a whole `todo` entity
// this component stays disconnected from the useQuery in the parent TodoList.
// useEntity creates a separate scope for every Todo so changes to TodoList
// or sibling Todos don't trigger unnecessary re-renders.
const Todo = React.memo(({ id }) => {
const [todo] = useEntity(id)
return (
<div>
<TodoProject todo={todo} />
&nbsp;·&nbsp;
<TodoOwner todo={todo} />
&nbsp;·&nbsp;
<TodoDelete todo={todo} />
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'flex-end', paddingTop: 20}}>
<TodoCheck todo={todo} />
<TodoName todo={todo} />
</div>
<div>
<TodoProject todo={todo} />
&nbsp;·&nbsp;
<TodoOwner todo={todo} />
&nbsp;·&nbsp;
<TodoDelete todo={todo} />
</div>
<small style={{ color: 'grey' }}>
{todo.get('createdAt').toLocaleString()}
</small>
</div>
<small style={{ color: 'grey' }}>
{todo.get('createdAt').toLocaleString()}
</small>
</div>
)
)
})

const TodoCheck = ({ todo }) => {
const [transact] = useTransact()
Expand All @@ -207,12 +161,7 @@ const TodoCheck = ({ todo }) => {
type="checkbox"
style={{ width: 20, height: 20, cursor: 'pointer' }}
checked={!!todo.get('isCompleted')}
onChange={e => transact([{
todo: {
id: todo.get('id'),
isCompleted: e.target.checked
}
}])}
onChange={e => transact([{ todo: { id: todo.get('id'), isCompleted: e.target.checked } }])}
/>
)
}
Expand All @@ -222,10 +171,10 @@ const TodoName = ({ todo }) => {
return (
<input
style={{
border: 'none', fontSize: 20, marginTop: -2, cursor: 'pointer',
border: 'none', fontSize: 20, marginTop: -2, cursor: 'pointer',
...todo.get('isCompleted') && { textDecoration: 'line-through '}
}}
value={todo.get('name')}
defaultValue={todo.get('name')}
onChange={e => transact([{ todo: { id: todo.get('id'), name: e.target.value }}])}
/>
)
Expand All @@ -234,41 +183,24 @@ const TodoName = ({ todo }) => {
const TodoProject = ({ todo }) => {
const [transact] = useTransact()
return (
<ProjectSelect
value={todo.get('project', 'id') || ''}
onChange={projectId => transact([{ todo: { id: todo.get('id'), 'project': projectId || null }}])}
<EntitySelect
label="Project"
entityType="project"
value={todo.get('project', 'id')}
onChange={project => transact([{ todo: { id: todo.get('id'), project }}])}
/>
)
}

const TodoOwner = ({ todo }) => {
const [transact] = useTransact()
const [users] = useQuery({
$find: 'user',
$where: { user: { name: '$any' } }
})
return (
<>
<label>
Owner:
</label>
&nbsp;
<select
name="users"
value={todo.get('owner', 'id') || ''}
onChange={e => transact([{ todo: { id: todo.get('id'), owner: Number(e.target.value) || null }}])}
>
<option value=""></option>
{users.map(user => (
<option
key={user.get('id')}
value={user.get('id')}
>
{user.get('name')}
</option>
))}
</select>
</>
<EntitySelect
label="Owner"
entityType="user"
value={todo.get('owner', 'id')}
onChange={owner => transact([{ todo: { id: todo.get('id'), owner }}])}
/>
)
}

Expand All @@ -279,4 +211,57 @@ const TodoDelete = ({ todo }) => {
Delete
</button>
)
}
}

const TodoFilters = () => {
const [filters] = useEntity({ identity: 'todoFilters' })
const [transact] = useTransact()
return (
<div>
<label>Show Completed?
<input
type="checkbox"
checked={filters.get('showCompleted')}
onChange={e => transact([{ todoFilter: { id: filters.get('id'), showCompleted: e.target.checked }}])}
/>
</label>
&nbsp;·&nbsp;
<EntitySelect
label="Project"
entityType="project"
value={filters.get('project')}
onChange={project => transact([{ todoFilter: { id: filters.get('id'), project }}])}
/>
&nbsp;·&nbsp;
<EntitySelect
label="Owner"
entityType="user"
value={filters.get('owner')}
onChange={owner => transact([{ todoFilter: { id: filters.get('id'), owner }}])}
/>
</div>
)
}

const EntitySelect = React.memo(({ label, entityType, value, onChange }) => {
const [entities] = useQuery({
$find: entityType,
$where: { [entityType]: { name: '$any' } }
})
return (
<label>{label}:&nbsp;
<select
name={entityType}
value={value || ''}
onChange={e => onChange && onChange(Number(e.target.value) || null)}
>
<option key="-" value=""></option>
{entities.map(entity => (
<option key={entity.get('id')} value={entity.get('id')}>
{entity.get('name')}
</option>
))}
</select>
</label>
)
})
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"test:js": "yarn build && jest js/tests",
"test:js:dev": "yarn build:dev && jest js/tests",
"test:cljs": "shadow-cljs compile test && node out/node-tests.js",
"test:cljs:watch": "shadow-cljs watch test-autorun",
"test": "yarn test:cljs && yarn test:js",
"test:dev": "yarn test:cljs && yarn test:js:dev",
"report": "rm -rf dist && shadow-cljs run shadow.cljs.build-report npm report.html",
Expand Down
4 changes: 4 additions & 0 deletions shadow-cljs.edn
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
:output-to "out/node-tests.js"
:ns-regexp "-test$"
:autorun false}
:test-autorun {:target :node-test
:output-to "out/node-tests.js"
:ns-regexp "-test$"
:autorun true}
:npm {:target :npm-module
:output-dir "dist/js"
:entries [homebase.react]
Expand Down
Loading
0