Traph
Installation
yarn add react-traph
Example
ReactDOM.render
User guide
Creating and using a traph tree
The primary function of this library is traph
. It creates a new store-like object we refer to as a graph (although it is technically a tree). The two primary fields exposed by a graph is the Provider
React node and the useGraph
-hook.
When traph
is called, a new React Context used to contain all the data for the graph is created. All subgraphs are also recursively instantiated.
Let's use traph to create a very simple TODO-app. First we create the graph:
// TodoGraph.js import traph from 'traph' const TodoGraph =
Then we create our components:
import React useState from 'react' // Todo.jsx{ <Todo = = > text </Todo>} // Todos.jsx{ // This fetches the entire graph-data, i.e. // { items: Array } const todos setTodos = TodoGraph { } } return <ul> todositems </ul> }
// CreateTodoForm.jsx{ const todos setTodos = TodoGraph const todoText setTodoText = { e; if !todoText return } return <form => <input ="text" = = /> <button>Add </button> </form> }
And finally we wrap the components in a provider and render it
// index.jsximport React from 'react'import ReactDOM from 'react-dom'import CreateTodoForm from './CreateTodoForm'import Todos from './Todos'import TodoGraph from './TodoGraph' return ReactDOM
Querying specific parts of the graph
In the example above, the <Todos />
-component only really cares about the items
-entry of the state. We can query this part of the graph specifically, by supplying an argument to the useGraph-function:
// Todos.jsx{ const items setItems = TodoGraph { } return <ul> items </ul> }
traph ensures that when setItems is called, the correct part of the graph is updated.
For more deeply nested structures, we can use a dot to separate what part to query. Internally, lodash get is used here. Hence for a structure like
const Graph =
We could now get the second user by executing
const user setUser = Graph
Updating the user with setUser
will merge the new data into the graph as expected.
Member functions
When calling useGraph
, we automatically get a function to update the state we have just received.
However, some people (yes, I'm looking at you) prefer to use member functions of their state to mutate this
. To avoid having to implement an observer-observable pattern on all attributes of a state, traph rebinds all functions in a graph, and makes available an updateGraph
-method on this
:
const Toasts =
Note also that all the graph's current values are injected into this
.
The updateGraph
-method is literally the same method as is returned when calling Toasts.useGraph()
.
Using non-objects as values
In the above examples, we have used objects as the initial value to traph. However, traph can store any data you wish to throw at it.
const Users = const IsOffline = const MeasurementData = const NumberOfUsersOnline =
Nested trees
Basic usage
One of the strengths of traph is the ability to nest graphs. Nested graphs can be used both independently and jointly.
Suppose we have:
const SidebarGraph = const ToastGraph = const Graph =
One of the advantages of nesting the graphs inside a top-level graph, is that the top-level Provider automatically will create the context-providers of the subgraphs:
<Graph.Provider> /* Graph.Provider renders ToastGraph.Provider and SidebarGraph.Provider automatically */ <ComponentConsumingAllGraphData /></Graph.Provider>
Within <ComponentConsumingAllGraphData />
, we can query, for instance, the data in ToastGraph
in two distinct ways:
{ const toastGraph setToastGraph = ToastGraph // or const toastGraph setToastGraph = Graph ...}
These are, in this scenario, equivalent. However, they may return differing results if the top-level Graph store is altered in a way which removes the toasts subgraph.
Querying into sub-graphs
We can query into subgraphs in the expected manner. Suppose we are given the Graph above.
{ // Will return the first element of the `toasts` property in the `toasts`-subgraph. const firstToast setFirstToast = Graph ...}
Automatic propagation of updated subgraph values
Suppose we have the following Graph:
const Graph =
and the following components:
{ const top setTop = Graph return <div> topmiddlebottomdeepValue </div> } { const middle setMiddle = Graph return <div> middlebottomdeepValue </div> } { const bottom setBottom = Graph return <div => bottomdeepValue </div> }
If the <div>
in <ComponentUsingBottom>
is clicked, all components receives the updated deepValue
automatically.
Any consumer of any graph above a graph being altered will also have its values updated.
Updating slices spanning multiple sub graphs
Expanding on the above example, we can wonder what happens if we update a slice of the entire graph from the consumer of the top graph. traph will in this scenario ensure that the data is propagated down into the subgraphs:
{ const top setTop = Graph return <div => topmiddlebottomdeepValue </div> } { const middle setMiddle = Graph return <div> middlebottomdeepValue </div> } { const bottom setBottom = Graph return <div => bottomdeepValue </div> }
As is expected, the Context in all the subgraphs are updated appropriately.
Overriding contexts
As traph is powered by React Contexts at its core, it is possible to have different values for a graph at different locations in the component hierarchy.
const Graph = { const message setMessage = Graph return <div => message </div> } ReactDOM
This would render two divs, the first with inner text "hello", and the second with inner text "override". Note that the graphData
property overrides the Graph initial data.
If the second div is clicked, only the second <Component>
has its value updated.
Attaching graphs to multiple locations
While the Graph data-structure returned by traph is technically a tree, it exhibits one piece of graph-like cyclic behavior, and that is the possibility to attach a graph to multiple locations within a tree.
const UserOne = const UserTwo = const Users = const HouseOne = const HouseTwo = const Houses = const Game =
This is perfectly valid — altough rather strange — and is handled appropriately by traph. If one of the users is consumed in any manner, and altered; this change is propagated to all other graphs/subgraphs which uses the user.
{ const users = Game return <div> users </div> } { const userOne setUserOne = Game return <div> <h2>First users area</h2> <span>Name: userOnename</span> <span =>Life: userOnelife</span> </div> } { const houses = Game return <div> houses </div> }
Clicking the life on the first users area will automatically update the UserOne graph's value in all the places where the graph is mounted.
Removing subgraphs
The structure of a graph's data is partially immutable after creation. Specifically,
the initial structure is stored separately from the mutated data. When a graph's data is consumed using useGraph
, this structure is used when resolving subgraph state. Hence,
you cannot really replace a subgraph with other data.
You can however disable the subgraph, by setting the corresponding key to null
.
const Graph = { const graph setGraph = Graph return <div => graphsubgraph ? "Subgraph exists" : "Subgraph does not exist" </div> }
Clicking the <div>
will make the subgraph disappear from the top-level graph's resolved data. Note that the Graph instance itself still exists, and setting the value back to something non-null (i.e. true) will re-enable the subgraph.
All the structure of the graph state which is not a subgraph is replaceable.
The third entry returned by useGraph
useGraph
actually returns three values, and not two. The third value is another updating function, which overwrites the data of the returned graph state, instead of doing a smarter merge.
const Store = { const user setUser replaceUser = dataGraph return <div> <button =>Update</button> <button =>Replace</button> <div> userfirstName userlastName usernewKey </div> </div> }
When clicking the first button, the new graph state will be
firstName: "Tormod" lastName: "Tormod" newKey: "newValue"
But when clicking the second button, the new graph state will be
newKey: "anotherNewValue"
The object passed to replaceUser
is accepted as the complete new state.
Quirks
There are a few quirks to be wary off when using this library:
- If you mount a Graph at multiple locations in an above Graph, it's Provider will only be rendered once, as the traph deduplicates providers.
- There are two locations where you can supply initial data to a graph: As the first argument to it's constructor, but also in the
graphData
prop of the graph's Provider. The latter will always dominate. - The internal data store (which is separate from the graph initial structure) is treated as immutable. Updating a complex data type (Object, Array), will always create a new object of the same type. However, in cases where new entries are added, these are added as-supplied to the graph.
- Functions located deeply within some data structure which is not a graph, will not be rebound to take the
updateGraph
method.
Behaviours explicitly listed as undefined by the library
The following behaviours are (currently) listed as undefined. Use at your own peril:
- Injecting subgraphs into a graph's state after creation.
- Adding complex object types (e.g. classes) with graphs located somewhere within the class.
- Adding functions as the initial data to a graph (the issue here is related to the fact that functions sent to React's useState internally calls the function).
TODOS
- Dynamically adding subgraphs.
- Rebinding of deeply nested functions.