Deeply watches your state-object and props for changes. Re-renders automatically๐ and makes you write much less code๐.
- Performance friendly
React Deepwatch uses a proxy-facade to watch only for those properties that are actually used in your component function. It doesn't matter how complex and deep the graph behind your state or props is. - Can watch your -model- as well
If a (used) property in props points to your model, a change there will also trigger a re-render. In fact, you can watch anything ;)
// Will reload the fruits and show a ๐ during load, if you type in the filter box.
const MyComponent = watchedComponent(props => {
const state = useWatchedState({
filter: "",
showPrices: false,
})
return <div>
{/* A nice bind syntax. No more 'onChange(...)' code */}
Filter <input type="text" {...bind(state.filter )} />
{/* state.filter="" will automatically rerender and re-run the following server fetch, if necessary๐ */}
<input type="button" value="Clear filter" onClick={() => state.filter = ""} />
{/* you can fetch data from **inside** conditional render code or loops๐! No useEffect needed! Knows its dependencies automatically๐ */}
<div>Here are the fruits, fetched from the Server:<br/><i>{ load(async ()=> await fetchFruitsFromServer(state.filter), {fallback:"loading list ๐"} )}</i></div><br/>
{/* The above load(...) code is independent of state.showPrices, react-deepwatch knows that automatically, so clicking here will NOT exec a re- load(...)๐... */}
Show prices <input type="checkbox" {...bind(state.showPrices)} />
{/* showing here, that clicking "show prices" will **only** do a rerender: */}
{state.showPrices?<div>Free today!</div>:null}
</div>
});
createRoot(document.getElementById('root')).render(<MyComponent/>);npm install --save react-deepwatchimport {watchedComponent, watched, useWatchedState} from "react-deepwatch"
const MyComponent = watchedComponent(props => {
const state = useWatchedState( {myDeep: {counter: 0, b: 2}}, {/* WatchedOptions (optional) */} );
return <div>
Counter is: {state.myDeep.counter}<br/>
<button onClick={ () => state.myDeep.counter++ /* will trigger a rerender */ }>Increase counter</button>
</div>
}, {/* WatchedComponentOptions (optional) */});
<MyComponent/> // Use MyComponentNow that we already have the ability to deeply record our reads, let's see if there's also a way to cut away the boilerplate code for useEffect:
import {watchedComponent, load, poll, isLoading, loadFailed, preserve, READS_INSIDE_LOADER_FN} from "react-deepwatch"
const MyComponent = watchedComponent(props => {
return <div>
Here's something fetched from the Server: { load( async () => await myFetchFromServer(props.myProperty), {/* LoadOptions (optional) */} ) }
</div>
});
<MyComponent/> // Use MyComponentNote: ๐ load(...) even can be inside a conditional block or a loop ๐.
The returned Promise will be await'ed and the component will be put into suspense that long.
load(...) re-executes myFetchFromServer, when a dependent value changes. For this auto-dependency mechanic to work, make sure, all sources to your component are watched: props and other load(...)'s result are already automatically watched; For state, use useWatchedState(...); For context, use watched(useContext(...)).
By default, everything prior to the load(...) statement in your code and immediately in your loaderFn is treated as a dependency. This is the perfectly safe and care free option for most use cases. But it can sometimes be too broad and lead to more reloads than necessary. Therefore, when you do performance sensitive fetches, you can fine-tune the dependencies:
Fine-tuning your deps
Just like with React's useEffect(..., [...yourDeps...]), You can explicitly specify the deps in the LoadOptions#deps array.
Additional, there is one special symbol which you can insert there: READS_INSIDE_LOADER_FN. This will treat trackable reads on objects in your loaderFn as dependencies. Examples:
const someWatchedObj = ... //somehow derived (deeply) from props, watched(...) or useWatchedState(...) or load(...)
const showPrices = state.showPrices;
{load(async () => {return await fetchFruitsFromServer(showPrices)}) } // โ
Auto dependencies. This will include the recorded "read" in the line 'const showPrices = state.showPrices;' and therefore reload when showPrices is different (=any determnistic value that's derived from the inputs till here, which covers everything you could need๐).
{load(async () => {return await fetchFruitsFromServer(state.showPrices )}, {deps: [READS_INSIDE_LOADER_FN ]}) } // โ
{load(async () => {return await fetchFruitsFromServer(someWatchedObj.users.get("Axel").getPrefs().showPrices)}, {deps: [READS_INSIDE_LOADER_FN]}) } // โ
Depending on properties, starting from **on** a watched object/Array/Set/Map or derived stuff is all tracked (Thanks to proxy-facades, it will follow and track this complete path of reads and watch for changes precisely there๐).
{load(async () => {return await fetchFruitsFromServer(showPrices )}, {deps: [READS_INSIDE_LOADER_FN ]}) } // โ Reading the closured variable showPrices is not trackable by react-deepwatch. It can only track reads **on** (proxied-) objects = You should see a `.` or a `[...]`
{load(async () => {return await fetchFruitsFromServer(showPrices )}, {deps: [READS_INSIDE_LOADER_FN, showPrices]}) } // โ
List showPrices additionally as depExplicitly. Note: READS_INSIDE_LOADER_FN is not needed here, but it doesn't hurt.
{load(async () => {
const fruits = await fetchFruitsFromServer(); // Takes some time...
return fruits.filter(f => f.indexOf(state.myFilter) >=0); // โ READS_INSIDE_LOADER_FN cant't catch the state.myFilter read, cause it did not happen **immediately** but after an async fork/not in the same sync block. You have to keep this in mind. Also for the auto-dependencies.
}, {deps: [READS_INSIDE_LOADER_FN]}) }To show a ๐loading spinner / placeholder during load, either...
- wrap your component in a
<Suspense fallback={<div>๐</div>}>...<MyComponent/>...</Suspense>. It can be wrapped at any parent level๐. Or... - call isLoading() inside your component, to probe if any or a certain
load(...)statement is loading. See jsDoc for usage example. Mind the caveat of not using it for a condition to cut off a load statement. and/or... - specify a fallback value via
load(..., {fallback:"๐"}).
either...
- wrap your component in a
<ErrorBoundary fallback={<div>Something went wrong</div>}>...<MyComponent/>...</ErrorBoundary>from the react-error-boundary package. It can be wrapped at any parent level๐.
It tries to recover from errors and re- runs theloaderFn, whenever a dependency changes. Note that recovering works only with the mentioned react-error-boundary 4.x and not with 3rd party error-boundary libraries. Or... - try/catch around the load(...) statement. Caveat: You must check, if caught is
instanceof Promiseand re-throw it then. Because this is the way forloadto signal, that things are loading. Or... - call the loadFailed() probing function. This looks more elegant than the above. See jsDoc for usage example.
Use the preserve function on all your fetched data, to smartly ensure non-changing object instances in your app (newFetchResult === oldFetchResult; Triple-equals. Also for the deep result_). Changed object instances can either cascade to a lot of re-loads or result in your component still watching the old instance.
Think of it like: The preserve function does for your data, what React does for your component tree: It smartly remembers the instances, if needed with the help of an id or key, and re-applies the re-fetched/re-rendered properties to them, so the object-identity/component-state stays the same.
๐ load(...) does preserve its result by default to enforce this paradigm and give you the best, trouble free experience.
- The component function might return and empty
</>on the first load and produce a short screen flicker. This is because React's Suspense mechasim is not able to remeber state at that time. To circumvent this, specifyWatchedComponentOptions#fallback. <Suspense>and<ErrorBoundary>inside your component function do not handle/catch loads in that same function. Means: You must place them outside to handle/catch them.- If your app is a mixed scenario with non-watchedComponents and relies on the old way of fully re-rendering the whole tree to pass deep model data (=more than using shallow, primitive props) to the leaves, mind disabling the WatchedComponentOptions#memo flag.
- SSR is not supported.
- startTransition is not supported (has no effect).
- As said: Keep in mind that
load(...)callspreserveon its result. It also invalidates (destroys) the "unused" objects. When they're not really unused any you are trying to access them, You'll get the proper error message how to disable it.
Since we have proxy-facades, we can easily hook on, whenever some deep data changes and don't need to manage that with callback- passing to child-child components. Let's say, we have a form and a child component, that modifies it. We are interested in changes, so we can send the form content to the server.
import {watchedComponent, watched, useWatchedState} from "react-deepwatch"
const MyParentComponent = watchedComponent(props => {
const myState = useWatchedState({
form: {
name: "",
address: ""
}
}, {onChange: () => console.log("Something deep inside myState has changed")}); // Option 1: You can hook here
return <form>
<ChildComponentThatModifiesForm form={ // let's pass a special version of myState.form to the child component which calls our onChange handler
watched(myState.form, {
onChange: () => {postFormToTheSerer(myState.form); console.log("Somthing under myState.form was changed")} // Option 2: You can also hook here
})
}/>
</form>
});This example will trigger both onChange handlers.
Note, that watched(myState.form) !== myState.form. It created a new proxy object in a new proxy-facade layer here, just for the purpose of deep-watching everything under it. Keep that in mind, when i.e. comparing objects by instance (I.e. row === selectedRow) if they arrived in different ways. Sometimes you may want to take advantage of it, so that modifications in the originaly layer (in MyParentComponent) won't fire the onChange event / call the postFormToTheSerer function. I.e. for updates that came from the server
Let's make the value binding code a bit easier:
import {bind} from "react-deepwatch";
// Old:
<input type="text" value={myState.myValue} onChange={(event) => myState.myValue = event.how.was.that.again.i.just.want.the.value} />
// New:
<input type="text" {...bind(myState.myValue)} />It works for all sorts of input types, not only text๐. Also with MUI and Blueprint input components and should work in general with other library components as long as they stick to the standards.
You can also use watched similarly to useWatchedState to watch any global object. But in React paradigm, this is rather rare, because values are usually passed as props into your component function.
Besides load, there's also the poll function, which works similar, but re-loads in regular intervals. See jsDoc
Besides load, react-deepwatch also supports hosting retryable-synchronous code.
There are also other libraries that address proxying the state:
valtio, react-easy-state, wana,
while React-deepwatch set's its self apart in these areas:
- Deep (not only shallow-) proxying
- Tracking changes above and below the proxy = also on the unproxied object.
- Full transparent support for
this, getters/setters (treated as white box), user's methods, Sets, Maps, Arrays (wana seems to support Sets,Maps, Arrays too) - Very comprehensive
load(...)concept with auto dependencies, fallbacks, probing functions, instance preserving mechanism, possible in conditionals/loops, supports polling, error boundaries. - <Input/> bind(...)ing
If you like, how this library simplifies things for you and want to write the backend (http) endpoints behind your load(...) statements simply as typescript methods, have a look at my flagship project Restfuncs. Example:
// In your watchedComponent function:
return <div>The greeting's result from server is: { load( async () => await myRemoteSession.greet(state.name) ) }</div>
// On the server:
...
@remote greet(name: string) {
return `Hello ${name}`
}
...The example leaves away all the the setup-once boilerplate code.
Also in your tsx, you can enjoy type awareness / type safety and IDE's code completion around myRemoteSession.greet and all its parameters and returned types, which is a feature that only rpc libraries can offer (Restfuncs is such one)๐