Skip to content

bogeeee/react-deepwatch

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

React Deepwatch - no more setState and less

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 ;)

Quick example to show you most features

// 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/>);

Open in StackBlitz

Install

npm install --save react-deepwatch

Usage

no more setState

import {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 MyComponent

Open in StackBlitz

and less... loading code

Now 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 MyComponent

Open in StackBlitz

Note: ๐Ÿ‘ 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(...)).

Dependencies

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]}) }

Show a ๐ŸŒ€loading spinner

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:"๐ŸŒ€"}).

Handle errors

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 the loaderFn, 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 Promise and re-throw it then. Because this is the way for load to signal, that things are loading. Or...
  • call the loadFailed() probing function. This looks more elegant than the above. See jsDoc for usage example.

Object instance preserving

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.

Caveats

  • 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, specify WatchedComponentOptions#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(...) calls preserve on 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.

And less... handing onChange listeners to child components

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>
});

Open in StackBlitz

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

...and less onChange code for <input/> elements

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)} />

Open in StackBlitz

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.

Further notes

watched

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.

poll

Besides load, there's also the poll function, which works similar, but re-loads in regular intervals. See jsDoc

Retsync

Besides load, react-deepwatch also supports hosting retryable-synchronous code.

Similar libraries

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

Simplify the server side as well

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)๐Ÿ˜Ž

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages