React bindings for destam
npm install destam-reactReact.useState semantics butA basic example of how to use useObserver
``jsx
// first we're going to create a number that increments every second
const number = Observer.mutable(1);
setInterval(() => {
number.set(number.get() + 1);
}, 1000);
const Component = () => {
// every time the number is incremented by the setInterval, the component
// will automatically rerender with the new value. The user should see
// a number count up. To keep with React.useEffect: we also get a setCount
// value as well.
const [count, setCount] = useObserver(number);
return
`
This code would do something similar to if you used React.useState and aReact.useEffect to create a timer. The difference is that the observerComponent
solution has a global counter so all instances of created will
render the same number. If the count was reset for one component, it would
reset for all components. That's really the power of observers here is to share
state that the rest of the program manages.
useObserver provides a couple of interfaces. Above we use a basic interface
with use with an observer. All cases should be covered by this but sometimes
some sugar would be nice.
`jsx`
const state = OObject({ value: 'value' });
const [value, setValue] = useObserver(state, 'value');
The above snippet is sugar for Observer.prototype.path it will simply listenvalue
for the state to change. This paramater can instead be an array which
would have the observer listen along a chain of events like this:
`jsx`
const [value, setValue] = useObserver(state, ['user', 'name']);
Sometimes it's important to construct an observer specifically for a useObserver
call. The helpers above can help for common cases, but there can be more complex
cases. Consider you want to define a default variable for an observer that would
normally be undefined.
`jsx`
const [value, setValue] = useObserver(() => state.observer.path('value').def('default'), [state]);
useObserver does not provide a helper for creating an observer that will resolveuseMemo
to a default value. We have to create that observer ourselves. It's important
that the observer is created through a callback because it means we won't get
confused when the app remounts and sees a different observer. Remember that
identical calls to the same observer will create different references to observers
in memory. It's more like a hidden combined for us here. Like useMemo,
we can also give it a dependency array:
`jsx`
const [value, setValue] = useObserver(() => state.observer.path('value').def(default), [state, default]);
A common mistake made with observers is to create them every time your component
remounts. The point of observers is for their lifetime to typically be the lifetime
of the state they represent. You don't want observers to be created and destroyed
to manage the same piece of state. Consider this:
`jsx
const Component = (defaultName) => {
const observer = Observer.mutable(defaultName);
const [name, setName] = useObserver(observer);
return
The above component would not work because the observer is being recreated every
time the component mounts. It would always take teh value of defaultName regardless
if the user changes what's in the textarea. The way to fix this is to use
React.useMemo.`jsx
const Component = (defaultName) => {
const observer = React.useMemo(() => Observer.mutable(defaultName), [defaultName]);
const [name, setName] = useObserver(observer); return
Your name is {name}
;
};
`Now the component will work as expected. Always memo your observers if you ever
need to create them inside your component!
Using useEffect with Observer.prototype.watch
As flexible and easy to use is
useObserver it's sometimes not powerful enough
or you want to optimize for component rerenders. An easy way to reduce the
amount of times your component rerenders is to identify observer state that
is only used in a useEffect but is not used in the jsx. Consider this:`jsx
const Component = () => {
const query = React.useMemo(() => Obserever.create(null), []); const [queryValue] = useObserver(query);
const [searchResults, setSearchResults] = React.useState([]);
React.useEffect(() => {
setSearchResults(getSearchResults(queryValue));
}, [queryValue]);
return
;
};
`Notice that nothing inside the component rendering needs queryValue itself, but
we are using
useObserver with the query that can change every time the user
types. Instead we can use the watch primitive directly in the use effect.
`jsx
const Component = () => {
const query = React.useMemo(() => Obserever.create(null), []); const [searchResults, setSearchResults] = React.useState([]);
React.useEffect(() => {
const update = () => {
setSearchResults(getSearchResults(query.get()));
};
update();
const listener = query.watch(update);
return () => {
listener.remove();
}
}, [queryValue]);
return
;
};
`Note that sometimes instead of:
`js
const listener = query.watch(update);return () => {
listener.remove();
}
`
You can return the remove function directly. These two things do the same things.
The problem with the below solution is that it's not trivial to add other
things that need to be cleaned up with the useEffect.`js
return query.watch(update).remove;
`Complete example
Counter
`jsx
const ShowCounter = ({count: countObs}) => {
const [count] = useObserver(countObs); return
The count is at: {count}
;
};const Counter = ({state}) => {
return
;
};const state = OObject({
// we're going to initialize the counter
// so we don't try to increment undefined
count: 0
});
createRoot(document.getElementById('root')).render( );
`Todo
`jsx
const TodoItem = ({item}) => {
const [name] = useObserver(item, 'name');
const [completed, setCompleted] = useObserver(item, 'completed'); return
style={{textDecoration: completed ? 'line-through' : 'none'}}
onClick={() => {
setCompleted(c => !c);
}}
>
{name}
;
};const TodoList = ({todos}) => {
const [items] = useObserver(todos);
return
{items.map((item, i) => {
return ;
})}
;
};const AddTodo = ({todos}) => {
const [current, setCurrent] = React.useState('');
return
setCurrent(e.target.value)} />
;
};const TodoFilter = ({filter}) => {
const [filt, setFilt] = useObserver(filter);
return
Show:
;
};const Undo = ({state}) => {
const [history, setHistory] = React.useState([]);
const [historyPos, setHistoryPos] = React.useState(0);
const network = React.useMemo(() => createNetwork(state.observer), [state]);
React.useEffect(() => () => network && network.remove(), [network]);
React.useEffect(() => {
return state.observer.watchCommit((commit, args) => {
if (args === 'is-undo-action') {
return;
}
setHistoryPos(pos => {
setHistory(history => history.slice(0, pos).concat([commit]));
return pos + 1;
});
}).remove;
}, [state]);
return
;
};const Todo = ({state}) => {
return
{
return todos.filter(todo => {
if (filt === 'completed' && !todo.completed) return false;
if (filt === 'active' && todo.completed) return false;
return true;
});
})}/>
All items
;
};const state = OObject({
// we're going to initialize the counter
// so we don't try to increment undefined
todos: OArray(),
filter: 'all',
});
createRoot(document.getElementById('root')).render( );
`Checkboxes
`jsx
const Checkbox = ({value, name}) => {
const [checked, setChecked] = useObserver(value); return <>
>;
};
const countries = [
'Australia',
'Canada',
'France',
'USA',
'Mexico',
'Japan',
];
const App = () => {
const checkboxes = countries.map(name => ({name, value: Observer.mutable(false)}));
return <>
name='Check All'
value={Observer.all(checkboxes.map(c => c.value)).map(cbs => {
return !cbs.some(c => !c);
}, v => {
return Array(checkboxes.length).fill(v);
})}
/>
{checkboxes.map((checkbox, i) => {
return key={checkbox.name}
name={checkbox.name}
value={checkbox.value}
/>;
})}
>;
};
createRoot(document.getElementById('root')).render( );
``