Migrating
2.x to 3.0 Beta
Version 3.0 is a big change focused mainly in four areas. If you have any trouble with the migration join the community on Discord or Github and we will help.
1. Types
The types were fully rewritten from scratch, fixing many bugs especially around Promises and nullability, and many of the types were renamed. You may need to rename some of your type imports.
2. Computed/Proxy
computed/proxy are no longer separate concepts, they’re just a function in an observable. If you were using computed
you can just use observable
now, and if you were using computed
as a child of an observable, you can remove computed
and use just a function. The old behavior will still work for now, but the new way is more efficient and computed
will be deprecated in a later version.
See Computed Observables for more details.
proxy
is now just a function with a string parameter.
See Computed Observables for more details.
And you can use useObservable
in the same way that you used to use useComputed
:
3. Persist
Persistence works in the same way but the API is changed for clarity and new features with the new more robust sync system. persistObservable
is changed to syncObservable
and the persisting features are under the persist
option instead of local
.
Additionally, persistObservable
was inserting a state
property for the sync state, which caused compatibility issues with data that had its own state
property. So the sync state is now accessible separately with syncState
.
Global configuration was confusing and restricting, so instead of a global configureObservablePersistence
you can now create customize synced functions.
4. Sync
It’s now much easier to sync individual observables with a new plugin system. See the Persist and Sync docs for the latest documentation.
If you had made a custom sync plugin on the old system, please post an issue on Github or talk to us on Discord, and we will help you with the migration.
5. Removed old sync plugins
Version 2 had inconsistent and varied ways to sync with fetch or Query, and they are now aligned into the Sync system.
- Fetch: syncedFetch
- TanStack Query: syncedQuery
Other changes
- useObservable with a function parameter is now reactive like useComputed was. So use peek() when accessing observables inside it if you want it to be just an initial value and not be reactive.
- Computeds now only re-compute themselves when observed. This may cause some migration issues if your computeds had side effects, as they will not re-run when dependencies change unless being observed.
- Removed lockObservable: With the new method of computeds it’s not possible to modify the types to be readonly, so we removed this feature.
- set and toggle return void: They had previously returned the observable in order to allow chaining, but it caused unintended side effects, so they now return void.
onSet
was renamed toonAfterSet
for clarity- Removed the concept of “after batch” - it was generally unreliable because batches can run recursively
- Renamed
enableDirectAccess()
toenable$GetSet()
andenableDirectPeek()
toenable_PeekAssign()
for clarity - Moved
trackHistory
export, so you can now useimport { trackHistory } from '@legendapp/state/helpers/trackHistory'
1.x to 2.0
Version 2.0 removes old deprecated features to reduce bundle size and encourage everyone to move over to the new features. Version 1.11 displays deprecation warnings to help you migrate.
So there are two migration strategies:
- Runtime: Upgrade to version 1.11 and check the console for warnings whenever using deprecated features. This can give you time to do the migration slowly without breaking anything.
- Build time: Upgrade to version 2.0 and use TypeScript warnings to find errors
These are all things that were changed over time between 1.0 and 2.0 so depending on when you started using Legend-State you may already be doing it the new way.
Promise behavior changed
When setting a Promise into an observable it creates a state
child on the observable with isLoaded
and error
properties that you can check for load state. After it resolves or rejects it replaces itself with the resolved value and updates the state
. Previously in the case of an error it would replace itself with an { error }
object.
So if you had logic to check whether a Promise errored by checking the error
property on the observable, you can change that to .state.error
.
enableLegendStateReact, enableReactDirectRender => Memo
The direct rendering enabled by enableLegendStateReact
and enableReactDirectRender
was fragile, hard to find in files, and the React team advised against it. So instead we are using the Memo
component. See Render an observable directly for more details.
To migrate you can remove usage of enableLegendStateReact()
or enableReactDirectRender()
as well as any usage of direct rendering, and replace it with Memo
. When you remove those imports it will stop overriding the types so rendering observables directly will result in TypeScript errors.
If you were using enableLegendStateReact
to render direct observables heavily, enableReactDirectRender is still usable to ease migration, though it will throw TypeScript errors to help you migrate away. It will be fully removed in version 3.0.
Legend components changed to Reactive components
The reactive components are now better named and more easily customizable with configuration functions, exported from the normal /react
path. See Reactive components for more details.
Change:
To:
For parameter name changed
As we’ve been adopting a convention of naming observables suffixed with $
, many users were confused by the item
prop in the render component used in For’s item
prop, so we renamed it to item$
to make it clear that it’s an observable. item
will still work until version 3.0, but it will throw TypeScript errors to encourage changing it to item$
.
Reactive props changed to start with $
In an earlier version reactive props ended with $ and were changed in version 1.3.0 to allow starting with $, because it has a better UX with autocomplete and is easier to visually scan for. Both have been supported but version 2.0 will remove type support for the suffix version. It will still work in code so it doesn’t break your apps, but we will fully remove it in 3.0 and suggest you at least start using the new pattern.
A recommended way to find and replace all of instances of the old method is to find $=
in all files.
Persistence Changes
In version 2 we locked down and cleaned up the interfaces for the remote persistence APIs.
persistObservable returns an object with sync state
To support taking an initial state or an observable, persistObservable
needs to return both an observable and the syncState, so it now the observable with a state
property on it, matching the Promise behavior.
See persistObservable for details.
persistLocal => pluginLocal
We renamed the parameters for clarity as the difference between local
and persistLocal
wasn’t clear. So it is now named pluginLocal
because that makes more sense.
Remote options renamed
Since there were not any remote persistence plugins before, these changes would likely not affect you unless you made your own. See Remote Persistence for details.
observer, reactive, reactiveObserver not exported from react-components
The /react-components
export was mistakenly exporting observer
, reactive
, and reactiveObserver
which are already exported from /react
. Your editor may have automatically imported from /react-components
so may need to be changed.
types.d.ts moved to types/babel.d.ts
types.d.ts
was too generic of a name now that we have a lot of configuration options, so we are naming them more specifically in a “types” folder. For now there’s still only the babel
types but this gives room to add more in the future.
afterBatch removed
afterBatch
was not working 100% correctly in all cases, and the best way to fix it was to make it part of batch(...)
.
0.23 to 1.0
onChange changed
-
onChange
now takes a second object parameter withtrackingType
andinitial
options. If you were using a second parameter (liketrue
to track shallowly) before, use{ trackingType: true }
. -
The
onChange
callback now receives an object withvalue
,getPrevious
, andchanges
in it, replacing the previous multiple arguments.
These changes allow for more flexibility - it’s easier for callers who care about the changes but not the current value or previous values (like persistence plugins), and the new initial
option lets it behave more like observe
where it runs immediately instead of waiting for a change.
when and Show tweaked
They were previously checking if the value is “ready”, meaning it doesn’t count if it’s an empty object or empty array. They now do a standard javascript truthiness check as would be expected. For the previous behavior you can use whenReady
or <Show ifReady={...}>
IndexedDB preloader removed
It was actually slower in our testing so we simplified things and just removed it. See IndexedDB for up-to-date docs.
0.22 to 0.23
Setting an observable object to the same value no longer notifies
Setting an object to itself was triggering notifications, which is not great for performance and is undesirable in most cases. It is now more targeted and will only notify on elements that actually changed. It’s unlikely that will affect you, but it may be a breaking change for you if you depended on things re-computing/re-rendering even if nothing changed.
Not automatically treating DOM nodes and React elements as opaque objects
It was adding most likely unnecessary extra code and is easily solved in a more generic way. If you’re storing those in observables, wrap them in opaqueObject(...)
.
IndexedDB plugin support for non-dictionaries removed
For flexibility of multiple observables persisting to the same IndexedDB table, it now has an itemID
option to save non-dictionaries. So the IndexedDB persistence plugin can be used in two ways:
- Persisting a dictionary where each value has an
id
field, and each value will create a row in the table - Persisting multiple observables to their own rows in the table with the
itemID
option
0.21 to 0.22
Local Storage is no longer the default persistence
This was changed to reduce build size for those who don’t use it. If you want to use Local Storage, configure it at the beginning of your app:
Moved persist plugins to /persist-plugins
Update your import paths:
when
is not triggered by empty or []
If you wanted when
to be triggered by those, you can update it to use a selector to return a boolean like:
0.20 to 0.21
Changed onChange
callback
The extra paremeters in the onChange
callback have changed to include an array of of the changes, fixing a bug where it was only showing the latest child’s change when changing multiple children while batching. This likely won’t affect many of you as it’s mostly intended for internal use and persistence plugins.
Renamed React components from legend
to Legend
We had originally used lower casing to match html elements, but in practice it did not autocomplete well and felt wrong. So just rename to uppercase, for example from <legend.div />
to <Legend.div />
.
0.19 to 0.20
Removed deprecated automatic observing
The automatic observing from 0.18 was deprecated in 0.19 and is now removed. See Deprecated automatic observing.
observe and useObserve
As observe
has gotten more and more powerful, it outgrew modifying behavior based on the return value, so it now has an event parameter to control canceling listening and a cleanup function.
- If you were returning false to cancel observing, you can now use
e.cancel = true
. - If you were returning a cleanup function you can use
e.onCleanup = () => ...
. - It also adds a
num
param to know how many times it’s run and aprevious
param to compare to the previous value.
Renamed event dispatch
to fire
Just change evt.dispatch()
to evt.fire()
and all is good đź‘Ť.
0.18 to 0.19
Deprecated automatic observing
We are deprecating the automatic observing that depended on hooking into React’s internals. Components will no longer track observables automatically, but you can easily it per component in a few ways:
- Wrap components in
observer
to make them track automatically - Wrap observable access in
useSelector
to return a value and track automatically. - Render observables directly into JSX.
So tracking observables in React can look like this now:
or
See the React guide for how we suggest setting up your components now.
Rendering observables directly still works though, and enableLegendStateReact()
still enables that.
You can still enable the previous behavior for now with enableLegendStateReact({ autoTrackingDEPRECATED: true })
while you migrate to using observer
or useSelector
. That option will be removed before we reach 1.0.
Why
- It doesn’t actually work. We thought this method would be safe to use because it was inspired by Preact Signals, but as we’ve integrated Legend-State into more environments we found significant edge cases that seem to be unfixable and suggest that the whole concept is just unworkable.
- The React team has asked us not to do it and made it clear that it is likely to break in a future version of React.
- As Legend-State has evolved, the ideal way of using it has shifted towards fine-grained reactivity where components render minimally or only once, and we were actually specifically opting out of auto-tracking more often than not. So in the interest of pursuing the render-once ideal, we think it’s actually generally better to use the reactivity components or opt-in to tracking.
- We don’t want to distract from the core mission of Legend-State with an unreliable and unstable core.
Bindable components deprecated
We now have a more general purpose way of making reactive props that can also be used for two-way binding for inputs. So change:
to
See reactive props for more detauls.
value
is no longer exposed
Primitives no longer have a value
that you could access and modify. We had previously removed that from the documentation and it is now removed from the code. You can just get()
and set()
as you would any other observable. It turned out to cause more bugs than it was worth and made the TypeScript types overly complex.
Removed get(false)
Use peek()
instead.
0.17 to 0.18
The tracing functions are renamed to use* to be inline with hooks:
- useTraceListeners
- useTraceUpdates
- useVerifyNotTracking
- useVerifyOneRender (new)
0.16 to 0.17
Primitives are now returned as observables
Observables previously tried to be clever by returning primitives directly, which was great in making it easy to work with state directly. But especially as the goal has moved more towards fine-grained reactivity, the balance shifted towards observable objects being better. So accessing primitives through state now returns observables like anything else.
Raw primitives:
- Pro: Easy to work with
- Con: Required
obs()
to get the observable to pass to props or render directly - Con: Easy to track a value without realizing it
Observable primitives
- Pro: More consistent
- Pro: Easier to deal with undefined
- Pro: Can dot through undefined paths easily
- Pro: Doesn’t need
obs()
or set by key - Pro: Easier to use fine-grained features without
obs()
everywhere - Pro: Easier to pass as props without needing
obs()
- Con: Requires
get()
for primitives
Changes to make:
get()
Wherever you were accessing primitives directly, add a .get()
to the end of it.
set(key, value)
Change set by key to access the node first. It will now work fine if the node is undefined.
From: state.profile.set('name', 'Annyong')
To:Â Â Â Â Â state.profile.name.set('Annyong')
obs()
Just remove it. The default behavior is now the same as what obs()
did before.
Hooks renamed
useComputed
is now useSelector
, re-rendering only when the return value changes.
useComputed
now returns a computed
observable.
0.15 to 0.16
enableLegendStateReact() to observe, removed observer
Legend-State now automatically tracks observable access in any component. To set it up, just call enableLegendStateReact()
at the beginning of your app.
Now observer
is no longer needed, so just remove all usage of observer
.
0.14 to 0.15
Safety
There are now three levels of safety: Unsafe, Default, and Safe. Default is new and allows direct assignment to primitives but prevents directly assigning to everything else. The previous default behavior was Unsafe so you may see errors if you were directly assigning to objects/arrays/etc… TypeScript should show errors so it should be easy to find them. Replace those with .set(...)
or pass in false
as the second parameter to observable
to go back to “Unsafe” mode.
Renamed ref to obs
ref
was a bit unclear and conflicted with React - the new feature to directly render observables requires a ref
property. So it is now renamed to obs
, which feels more intuitive as it is used to get an observable.
Array optimizations
The array optimizations are now opt-in, because they are only useful in React and can potentially have some unexpected behavior in React if modifying the DOM externally. You can enable them by using the For
component with the optimized
prop. See Arrays for more.
Shallow
Since there’s now a additionally the optimized
tracking for arrays, the shallow option on get()
and obs()
now has another option. So instead of passing shallow
to an observable, use the Tracking
namespace now.
Batching
The observableBatcher
namespace is removed and the batching functions are now exported on their own.
Change functions => observe/when
The new observe
and when
functions can automatically track all observables accessed while running them. This made the old extra change utilities unnecessary, so onTrue
, onHasValue
, onEquals
, and onChangeShallow
have been removed, saving 200 bytes (7%) from the bundle size. These are the new equivalents:
Primitive current => value
Primitive observables are now wrapped in { value }
instead of { current }
. You can also now modify the value
directly.
Renamed observableComputed and observableEvent
observableComputed
is now just computed
and observableEvent
is now just event
.
Renamed LS to Bindable
The automatically bound exports are now named better and in their own exports, so change your exports from LS
to:
Renamed Isolate to Computed
The control flow component Isolate
is renamed to Computed
for naming consistency.
Removed memo and isolate props
We found these confusing in practice as it wasn’t super clear when a component was getting memoized, and it’s not much extra work to use the Memo and Computed components directly. If you were using those, switch to the Computed and Memo components instead