-
Notifications
You must be signed in to change notification settings - Fork 2
Description
In order to simplify initializing a local vs external Arbor store with async data, we propose:
- Implementing
Arbor#loadwhich sets the store's state to the result of the given promise; - Implement
useAsyncArborwhich takes: - An "initializer" function that returns a promise whose result is used to initialize the Arbor store;
- A dependency list that can be used to recompute the store's state when changed, similar to
useEffectdependencies. - Implement
useSuspendableArborwhich works similarly touseAsyncArborexcept it is integrated with Suspense so users don't have to check for loading states.
Thoughts
Without Suspense
Without suspense, the DX would be similar to what it is commonly used by data fetching libraries out there.
The example below shows how that could look like when initializing a local Arbor store:
function TodoListView({ userId }: { userId: string }) {
const { data: todos, loading, error } = useAsyncArbor(() => fetch(`/${userId}/todos`), [userId])
if (error) // handler error state
if (loading) // handle loading state
return (
<ul>
{todos.map(todo => <TodoView todo={todo} />)}
</ul>
)
}
function TodoView({ todo }: { todo: Todo }) {
return (
<div>{todo.content}</div>
)
}A similar pattern could be used to initialize an external store with data resolved within the application's context:
// External store initialized somewhere in the application, outside React's lifecycle
const store = new Arbor(new TodoList())
function TodoListView({ userId }: { userId: string }) {
const { data: todos, loading, error } = useAsyncArbor(() => store.load(fetch(`/${userId}/todos`), [userId])
if (error) // handler error state
if (loading) // handle loading state
return (
<ul>
{todos.map(todo => <TodoView uuid={todo.uuid} key={todo.uuid} />)}
</ul>
)
}
function TodoView({ uuid }: { uuid: string }) {
// todoByUuid is a selector function (plain JS function) that is used to select a specific state tree node to subscribe to
const todo = useArbor(todoByUuid(uuid))
return (
<div>{todo.content}</div>
)
}With Suspense
When running within a suspense compatible React version, the pattern used is similar, but with less boilerplate since there will be no need for devs to explicitly check for loading states within their component logic.
This is what it would look like when initializing a local store:
function TodoListView({ userId }: { userId: string }) {
// Throws suspense promise
// Needs a suspense boundary around the component
const todos = useSuspendableArbor(() => fetch(`/${userId}/todos`), [userId])
return (
<ul>
{todos.map(todo => <TodoView todo={todo} />)}
</ul>
)
}
function TodoView({ todo }: { todo: Todo }) {
return (
<div>{todo.content}</div>
)
}And when initializing an external store:
const store = new Arbor(new TodoList())
function TodoListView({ userId }: { userId: string }) {
const todos = useSuspendableArbor(() => store.load(fetch(`/${userId}/todos`), [userId])
return (
<ul>
{todos.map(todo => <TodoView uuid={todo.uuid} key={todo.uuid} />)}
</ul>
)
}
function TodoView({ uuid }: { uuid: string }) {
const todo = useArbor(todoByUuid(uuid))
return (
<div>{todo.content}</div>
)
}Initialization via props
Since the initialization logic only expects a promise to resolve with the store's initial state, we can leverage the same mechanism in order to feed the store with data coming via props.
Changing the previous local store example to take the initial store data via props, we'd have:
function TodoListView({ initialTodos }: { todos: Todo[] }) {
const todos = useArbor(initialTodos)
return (
<ul>
{todos.map(todo => <TodoView todo={todo} />)}
</ul>
)
}
function TodoView({ todo }: { todo: Todo }) {
return (
<div>{todo.content}</div>
)
}Similarly, we can initialize an external store via props:
const store = new Arbor(new TodoList())
function TodoListView({ initialTodos }: { todos: Todo[] }) {
// The store initialization process happens only once and does not trigger a re-render.
const todos = useArbor(store.initialize(initialTodos))
return (
<ul>
{todos.map(todo => <TodoView uuid={todo.uuid} key={todo.uuid} />)}
</ul>
)
}
function TodoView({ uuid }: { uuid: string }) {
const todo = useArbor(todoByUuid(uuid))
return (
<div>{todo.content}</div>
)
}Initialize the store via props can be handy when working with SSR frameworks such as Remix or NextJs, where one can leverage the power of SSR to resolve the data from the backend while allowing having a client-side store that can provide the reactivity needed to build interesting UX solutions.
Simplifying the API
In order to maintain Arbor core values, we could look into making the API footprint as small as possible, provinding a single useArbor hook with overloaded behavior based on the arguments passed to it, so that devs don't have to learn multiple hooks in order to accomplish their tasks.