Introducing Legend-State 1.0: Build faster apps faster

May 16, 2023

After almost a year of development and iterating, we're very excited to announce that Legend-State has reached version 1.0! What started as a simple upgrade of Legend's state/sync system turned into a collaboration with Bravely, then into a quest for the best possible performance, and finally into a fundamental rethinking of React's developer experience (DX).

So in its 1.0 version, Legend-State has four main benefits:

  1. ⚡️ The fastest React state library
  2. 🦄 Very easy to use
  3. 🔥 Natural fine-grained reactivity
  4. 💾 Built-in persistence

Building three large apps on Legend-State (Legend's React app and Bravely's React and React Native apps) gave us a lot of room to iterate towards an optimal DX, and to optimize performance in all sorts of scenarios. Now that they're released and running in production, we're confident that Legend-State is ready for you to build your apps.

👋 Introducing Legend-State

Legend-State is a super fast and powerful React state library based on Observables. You may have heard a lot about Signals recently - Observables are conceptually similar but more powerful: deep reactive objects that notify listeners whenever anything in them changes.

Because we can listen for changes anywhere within an object, a React component can update only when a specific value changes while a persistence plugin can update whenever anything in the whole tree changes. Legend-State's Observables use Proxy in a unique way, tracking by path in an object, which makes it extremely fast and doesn't modify the underlying data at all.

Legend-State beats other React state libraries in both speed and memory usage. An optimized mode for arrays can re-render only changed elements instead of the whole array, making it significantly faster than is usually possible with React - see it on the left side of this table from the krausest benchmark.

🦄 As easy as possible to use

There is no boilerplate and there are no contexts, actions, reducers, dispatchers, sagas, thunks, or epics. Just call get() to get the raw data and set() to change it.

In React there is an observer HOC or a useSelector hook to get your data. Just access observables and your components update themselves automatically.

// Create an observable object
const state = observable({ settings: { theme: 'dark' } })

// Just get and set
state.settings.theme.get() === 'dark'
state.settings.theme.set('light')

// observe re-runs when accessed observables change
observe(() => {
    console.log(state.settings.theme.get())
})

// 1. Observer components automatically track observables and re-render when they change
const Component = observer(function Component() {
    const theme = state.settings.theme.get()

    return <div>Theme: {theme}</div>
})

// 2. Or the useSelector hook gets and tracks the observable, and returns the raw value
function Component2() {
    const count = useSelector(state.count);

    // Re-renders whenever count changes
    return <div>{count}</div>
}

While our primary goal is to achieve the fastest performance with the best possible DX, we're most excited about the easier way of working with React that it enables: to observe state changing rather than managing hooks re-running because of re-renders. You can now just skip that entire layer of React development entirely (if you want).

We're building apps much faster because the mental model is so much easier, and our apps have better performance without even thinking about it.

🔥 Fine-grained reactivity for minimal renders

We want our apps to be extremely fast and we also want them to be easy to build. So Legend-State makes fine-grained reactivity natural, resulting in fewer and smaller re-renders while actually removing complexity.

  • Text elements re-render themselves when they change
  • Reactive props update themselves when state changes them
  • Control-flow components re-render themselves when needed
  • Two-way binding for input elements

And they all do that without needing to re-render the component itself. Check out our previous post Making React fast by default and truly reactive for more on the motivation and benefits of this approach. But for now here's an example:

function Component({ children }) {
  // This component only renders once
  const state = useObservable({ show: false, toggles: 0, text: 'Change me' })

  useInterval(() => {
    state.show.set(v => !v)
    state.numToggles.set(v => v + 1)
  }, 1000)

  // Props ending in $ bind the prop to the observable
  // for tiny targeted re-renders on changes
  return (
    <div>
      <Reactive.input $value={state.text} />
      <Reactive.div
        $className={() => (
          state.text.get().includes('Legend') && 'font-bold text-blue-500'
        )}
      >
        {state.text}
      </Reactive.div>
      <div>
        Modal toggles: <Memo>{state.numToggles}</Memo>
      </div>
      <Show
        if={state.show}
        else={<div>Not showing modal</div>}
      >
        <div>Modal</div>
      </Show>
    </div>
  )
}
Renders: 1
Change me
Modal toggles: 0
Not showing modal

Of course you can just use observer or useSelector if you prefer, but this micro-updating is a great way to improve performance and feels very natural once you get used to it.

💾 Built-in Persistence

Application state almost always needs to be saved and synced, so Legend-State builds that in. You can persist an entire observable object or subtree with a single command.

// The state is automatically loaded at startup and saved on any change. Easy 😌.
persistObservable(state, {
    local: 'State',
    persistLocal: ObservablePersistIndexedDB,
})

Version 1 ships with plugins for Local Storage and IndexedDB for web and react-native-mmkv for React Native.

We also have a Firebase Realtime Database plugin which is the backbone of the sync systems for Legend and Bravely that we'll release soon, with hopefully more sync services to come. With that, your entire sync system could look like this:

persistObservable(userData, {
    persistLocal: ObservablePersistLocalStorage,
    persistRemote: ObservablePersistFirebaseDatabase,
    local: 'User',
    remote: {
        firebase: {
            syncPath: (uid) => `/users/${uid}/`
        }
    }
})

Remote sync is very fully featured - it works offline and resolves conflicts when coming online, can sync only the changes since the last update, supports encryption and field transforming, and more... But we'll get to that in a future update.

🤟 Getting started

Check out the documentation or the GitHub repo to get started with Legend-State. We would love to hear from you on GitHub, or talk to LegendApp or me directly on Twitter.

Or if you just want to try it out right now, here's a sandbox you can play with:

import { useObservable, Memo } from '@legendapp/state/react'
import { useRef } from 'react'
import { useInterval } from './useInterval'

export default function Counter() {
  const renderCount = ++useRef(0).current;
  const count = useObservable(1)

  useInterval(() => {
    count.set(v => v + 1)
  }, 600)

  return (
    <div>
        <div>Renders: {renderCount}</div>
        <div>Count: <Memo>{count}</Memo></div>
    </div>
  )
}

👉 What's next

We'll add our Firebase Realtime Database persistence plugin to the repo soon. Then we would like to add more persistence plugins for other services.

The fine-grained reactivity features are still a new paradigm even for us, so there's a lot more to explore there!

We have about a dozen helper observables like currentTime which updates every minute and hooks like useObservableNextRouter which enables easy hash-based routing in Next.js. This is just a start! We would love to have a massive library of helper observables and hooks, to reduce the amount of boilerplate required to build your apps.

If you'd like to help us with any of that please let us know on GitHub.


Making React fast by default and truly reactive

Sep 25, 2022

We love React and we've been very happily using it since 2015, but the dev experience and performance has always been held back by one fundamental flaw. We think we've found a way to solve it, to make React both:

  1. ⚡️ Much faster
  2. 🦄 Much easier to use

For all of its benefits and wonderful ecosystem, developing with React can feel needlessly complex, and React apps can be slow if you don't optimize correctly. We've found two main issues that can be much improved:

  1. 🐢 It's slow by default
  2. 🤯 Hooks are too complex

Of course React can be very fast, but achieving good performance requires wrapping components in memo, carefully ensuring props to children don't change with useCallback or useMemo, and managing dependency arrays. Hooks were a big step up from classes but they still create a lot of complexity. Making sure you're not using stale data, that variables are persistent across renders, and that dependency arrays do what you expect are a big nuisance that cause terrible bugs if you get it wrong.

These performance issues and confusing/frustrating experience all boil down to one fundamental problem:

React renders too much, too often.

By default, React always re-renders everything, all the way down. So React apps have a huge performance problem by default, because any little change re-renders everything.

function TreeLeaf({ count }) {
  return <div>Count: {count}</div>
}
function TreeLeft({ count }) {
  return <TreeLeaf count={count} />
}
function TreeRight() {
  return <div>Unrelated element</div>
}
function Tree() {
  const [count, setCount] = useState(1)

  useInterval(() => {
    setCount(v => v + 1)
  }, 600)

  return (<>
    <div>Count: {count}</div>
    <TreeLeft count={count} />
    <TreeRight />
  </>)
}
Passing count through props
Renders: 1
Count: 1
Intermediate
Renders: 1
Counter
Renders: 1
Count: 1
Unrelated element
Renders: 1

You might be thinking this is a silly example. It's so easy to optimize:

  1. You'd wrap each component in memo to keep them from re-rendering
  2. But the count prop is still changing so it needs to be in aContext instead of prop drilling
  3. Any click handlers would need to be wrapped in useCallback so they don't break memo
  4. No components can have children because that breaks memo
  5. Adding more state to the Context makes more components re-render on every change, so maybe you should use global state instead?

But that's the point. The performance problem is fixable, but the fixes are what cause the complexity. Because a functional component is just a function, all local variables are ephemeral by default. The workaround is Hooks, to keep local state and functions stable between renders. So the constant re-rendering causes two problems:

1. Unoptimized by default

The responsibility is on us developers to make it performant, to use memo to prevent children from re-rendering, and to make sure props don't change so that memo actually works. Fun fact: the children prop is always different, so memo is useless for components with children.

2. Need hooks to keep state/functions stable

The built-in hooks like useCallback, useMemo, useRef, useState, etc... exist for the sole purpose of keeping data consistent between renders. Dependency arrays make it your responsibility to tell React on which renders effects should run. Prop-drilling can obviate the benefit of memo so there's another workaround, Context, which adds another layer (or many nested layers) of complexity.

An enormous amount of the React experience is in compensating for the fact that components are constantly re-rendering, and those complex workarounds are the source of much of the mental overhead 🤯.

We've seen a lot of really cool new frameworks with different approaches to the rendering model solving some of these problems, like Solid and Svelte. But we really love the React ecosystem and React Native, so we tried to solve it within React.

Changing React to render only once

It doesn't make a lot of sense that changing one piece of text should have to re-render an entire component, or that showing an error message should re-render the whole component, or that editing an input should re-render its parent component on every keystroke.

We found that we could improve this without even changing React itself, but with just a state library.

So we built Legend-State to rethink the way we write React code. Rather than constantly re-rendering everything and working around it, components own the state they care about and re-render themselves when needed. Taking that even farther, the individual DOM nodes (or React Native Views) at the smallest and lowest level manage their own updating, so components never re-render.

Normal React
// This component renders every time
function HooksCount() {
  const [count, setCount] = useState(1)

  useInterval(() => {
    setCount(v => v + 1)
  }, 600)

  // Count changes every render
  return <div>Count: {count}</div>
}
Renders: 1
Count: 1
Legend-State
// This component renders once
function LegendStateCount() {
  const count$ = useObservable(1)

  useInterval(() => {
    count$.set(v => v + 1)
  }, 600)

  // Memo element re-renders itself
  return <div>Count: <Memo>{count$}</Memo></div>
}
Renders: 1
Count:
1

We can pass an observable straight into the JSX, and it automatically gets extracted to a tiny memoized component that tracks its own state. So it updates itself when it changes, and no other component needs to care about it.

The whole component doesn't need to re-render every time, only the one single text node.

So this is great! Text can re-render itself! But apps are obviously about a lot more than text. How do we handle more complex things like dynamic styles or conditional rendering?

🔥 Fine-grained reactivity

The conventional approach to optimizing React is to extract subtrees of JSX into separate components, wrap them in memo, and pass down all the props they need. That's much easier to do with observable than with useState because observables are stable references that won't trigger memoized components to render.

But still, it's just so cumbersome and time-consuming to do that everywhere, and it often isn't even possible.

Legend-State solves this with a Computed component, which isolates usage tracking away from the parent. Anything inside a Computed runs in its own state context, so the parent component never has to update.

Computed is a broad brush so we also have some more purposeful components like Show, Switch, For, and two-way binding with reactive props. These can be combined to optimize renders to only the tiniest elements that truly need updating.

Normal React
// This component renders every time
function HooksModal() {
  const [showChild, setShowChild] = useState(false)

  useInterval(() => {
    setShowChild(value => !value)
  }, 1000)

  return (
    <div>
      <span>Showing child: </span>

      <!-- Conditional className and text -->
      <span
        className={showChild ? 'text-blue' : ''}
      >
        {showChild ? 'true' : 'false'}
      </span>

      <!-- Conditional render -->
      {showChild && (
        <div>Child element</div>
      )}
    </div>
  )
Renders: 1
Showing child: false
Legend-State
// This component renders once
function LegendStateModal() {
  const showChild = useObservable(false)

  useInterval(() => {
    showChild.toggle()
  }, 1000)

  return (
    <div>
      <span>Showing child: </span>

      <!-- Runs in a separate tracking context -->
      <Computed>
        <span
          className={showChild.get() ? 'text-blue' : ''}
        >
          {showChild.get() ? 'true' : 'false'}
        </span>
      </Computed>

      <!-- Conditional runs in separate context -->
      <Show if={showChild}>
        <div>Child element</div>
      </Show>
    </div>
  )
}
Renders: 1
Showing child:
false

Components using this fine-grained rendering get a massive performance improvement by rendering less, less often. No matter how fast a Virtual DOM is, it's always faster to do less work.

But this optimization is only half of the goal here. Changing rendering to be optimized by default lets us remove a lot of the complexity and think about React in a simpler way.

🦄 An easier mental model

The mental model of React revolves around the render lifecycle. Hooks run on every render, so dependency arrays are needed to manage it. On the next render, old functions and variables become stale so we need useCallback and useState to keep them consistent between renders. Code running inside functions may be accessing stale variables, so we make them persistent with useRef. We've gotten used to it, but it's legitimately very complicated.

After tons of experimenting, we found that a more straightforward mental model is to observe state changing, not renders.

Cause => Effect

The mental model in React is not a clear cause => effect relationship. It's three steps: cause => effect => side effect.

  1. State changed, so
  2. The component re-rendered, so
  3. Run your code

But that second step is what causes all the madness 😱, so we just skip it. Without depending on the re-rendering step we have a simple cause => effect relationship:

  1. State changed, so
  2. Run your code

When actions aren't dependant on re-rendering, we don't need useEffect or dependency arrays anymore. Instead we have useObserve, which automatically runs whenever the state it observes changes. All you need to do is access state within it, and it tracks dependencies automatically.

Normal React
// This component re-renders on every keystroke
function HooksName() {
  const [name, setName] = useState('')

  // Trigger on render if name has changed
  useEffect(() => {
    document.title = `Hello ${name}`
  }, [name])

  // Handle change and update state
  const onInputChange = (e) => {
    setName(e.target.value)
  }

  // Controlled input needs both value and change handler
  return (
    <input value={name} onChange={onInputChange} />
  )
}
Legend-State
const profile = observable({ name: '' })

// This component renders once
function LegendStateName() {
  // Trigger when name changes
  useObserve(() => {
    document.title = `Hello ${profile.name.get()}`
  })

  // Two-way bind input to observable
  return (
    <Reactive.input $value={profile.name} />
  )
}

We find this much easier to reason about. There are no dependency arrays or extra hooks to keep things stable between renders. All that matters is when state changes, you do something. Cause => Effect.

Optimized by default

Combining all of the techniques we've discussed so far, React development becomes optimized by default because instead of defaulting to re-rendering the whole world, Legend-State re-renders only what really needs it. Because of that, all of the normal complexity around optimizing renders just disappears.

Revisiting the example from the beginning, the Legend-State version renders each component only once, because only the text of the count needs to change. You'll notice they're exactly the same except for useObservable instead of useState. We don't have to do anything crazy here. We don't even have to memo every component to optimize because they only render one time. It's just optimized by default without thinking about it.

Normal React
function TreeLeaf({ count }) {
  return <div>Count: {count}</div>
}
function TreeLeft({ count }) {
  return <TreeLeaf count={count} />
}
function TreeRight() {
  return <div>Unrelated element</div>
}
function Tree() {
  const [count, setCount] = useState(1)

  useInterval(() => {
    setCount(v => v + 1)
  }, 600)

  return (<>
    <div>Count: {count}</div>
    <TreeLeft count={count} />
    <TreeRight />
  </>)
}
Passing count through props
Renders: 1
Count: 1
Intermediate
Renders: 1
Counter
Renders: 1
Count: 1
Unrelated element
Renders: 1
Legend-State
function TreeLeaf({ count }) {
  return <div>Count: {count}</div>
}
function TreeLeft({ count }) {
  return <TreeLeaf count={count} />
}
function TreeRight() {
  return <div>Unrelated element</div>
}
function Tree() {
  const count = useObservable(1)

  useInterval(() => {
    count.set(v => v + 1)
  }, 600)

  return (<>
    <div>Count: {count}</div>
    <TreeLeft count={count} />
    <TreeRight />
  </>)
}
Setting observable
Renders: 1
Count:
0
Intermediate
Renders: 1
Counter
Renders: 1
Count:
0
Unrelated element
Renders: 1

This is of course much faster, but perhaps more importantly it lets us focus on making our apps and spend less time optimizing and figuring out dependency arrays and dealing with stale variables and making sure props don't change.

Even without all these fine-grained rendering improvements, Legend-State is faster than the other major React state libraries. In addition to minimizing renders, it's extremely optimized to be as fast as possible.

Replacing global state AND all of the React hooks

At its core Legend-State is a really fast and easy to use state library, so using it for global state is a great place to start.

But where it gets really powerful is as a replacement for the built-in hooks.

  • When we use useObservable instead of useState and useReducer, components stop re-rendering by default.
  • And then when components only render once, we don't need useMemo or useCallback anymore.
  • Since effects are not tied to rendering, we replace useEffect with useObserve to take actions when state changes.
  • For output that transforms other state, useComputed creates a computed observable that updates itself when dependencies change.
  • For data that comes in asynchronously, we can just give the Promise straight to useObservable and it will update itself when the Promise resolves.

In this more complex example of a basic chatroom, the Legend-State version creates self-updating computations and observers based on state, so it never has to re-render the whole component.

Normal React
// This component renders every keystroke
function HooksChat() {
  // Profile
  const [profile, setProfile] = useState()

  // Fetch and set profile
  useEffect(() => {
    fetch(url).then(response => response.json())
              .then(data => setProfile(data))
  }, [])

  // Username
  const userName = profile ?
    `${profile.first} ${profile.last}` :
    ''

  // Chat state
  const [messages, setMessages] = useState([])
  const [currentMessage, setCurrentMessage] = useState('')

  // Update title
  useEffect(() => {
    document.title = `${userName} - ${messages.length}`
  }, [userName, messages.length])

  // Button click
  const onClickAdd = () => {
    setMessages([...messages, {
      id: generateID(),
      text: currentMessage,
    }])
    setCurrentMessage('')
  }

  // Text change
  const onChangeText = (e) => {
    setCurrentMessage(e.target.value)
  }

  return (
    <div>
      {userName ? (
        <div>Chatting with {userName}</div>
      ) : (
        <div>Loading...</div>
      )}
      <div>
        {messages.map(message => (
          <div key={message.id}>{message.text}</div>
        ))}
      </div>
      <div>
        <input
            value={currentMessage}
            onChange={onChangeText}
        />
        <Button onClick={onClickAdd}>Send</Button>
      </div>
    </div>
  )
}
Renders: 1
Loading...
Legend-State
// This component renders once
function LegendStateChat() {
  // Create profile from fetch promise
  const profile = useObservable(() =>
    fetch(url).then(response => response.json())
  )

  // Username
  const userName = useComputed(() => {
    const p = profile.get()
    return p ? `${p.first} ${p.last}` : ''
  })

  // Chat state
  const { messages, currentMessage } =
    useObservable({ messages: [], currentMessage: '' })

  // Update title
  useObserve(() =>
    document.title =
      `${userName.get()} - ${messages.length}`
  )

  // Button click
  const onClickAdd = () => {
    messages.push({
      id: generateID(),
      text: currentMessage.get()
    })
    currentMessage.set('')
  }

  return (
    <div>
      <Show if={userName} else={<div>Loading...</div>}>
        <div>Chatting with {userName}</div>
      </Show>
      <For each={messages}>
        {message => (
          <div>{message.text}</div>
        )}
      </For>
      <div>
        <Reactive.input $value={currentMessage} />
        <Button onClick={onClickAdd}>Send</Button>
      </div>
    </div>
  )
}
Renders: 1
Loading...

As you type into the input in the normal version it re-renders the whole component and every child element on every keystroke, including every single item in the list.

Of course this is a silly example and you're probably already thinking of all the ways you'd optimize it.

  1. You'd extract the messages list to its own memoized component.
  2. You'd extract each message item to its own memoized component too.
  3. Adding the message does an expensive array clone - maybe there's a way to optimize that?
  4. You don't want the Button component to re-render every time so you wrap the click handler in useCallback. But then the state is stale in the callback, so maybe you save the state in a useRef to keep it stable?
  5. You could change the input to be uncontrolled with a useRef that you check for the current value on button click, so it doesn't re-render on every keystroke. But then what's the right way to implement a Clear button?

But that's the point. React is unoptimized by default, so you have to do a bunch of complex extra work to optimize it.

In the Legend-State version where we put the concept of re-rendering and all the hooks it requires behind us, React becomes optimized by default and easier to understand. It even takes less code than the most naive unoptimized React implementation.

What next?

Check out the documentation or the GitHub repo to get started with Legend-State. We would love to hear from you on GitHub, or talk to me directly on Twitter.

To get these benefits you don't need to immediately restructure your whole app or anything, and it's still just React so you don't need to change to a whole new framework. We've been migrating our apps gradually, just reducing re-renders in the slowest components and building new components with a one-render design. So you can just experiment with a single component and see how it goes. Or you could even try it right here in this sandbox:

import { useObservable, Memo } from '@legendapp/state/react'
import { useRef } from 'react'
import { useInterval } from './useInterval'

export default function Counter() {
  const renderCount = ++useRef(0).current;
  const count$ = useObservable(1)

  useInterval(() => {
    count$.set(v => v + 1)
  }, 600)

  return (
    <div>
        <div>Renders: {renderCount}</div>
        <div>Count: <Memo>{count$}</Memo></div>
    </div>
  )
}

I think there's a lot of room for rethinking the way we use React to be faster and easier. This is our first attempt at it, but I hope the community will come up with lots of wild and crazy new solutions!


React and React Native finally feel the same

May 23, 2022

If you're a developer of both React and React Native apps, it can be tough to switch between platforms because they feel so different. And in many ways React Native feels relatively... well, backwards.

Despite great improvements in DX (Developer Experience) for web development with animation libraries like Framer Motion and much easier styling with Tailwind CSS, React Native is still mired in the madness of creating StyleSheets and managing animation states with Animated or Reanimated.

Yes, I know it's basically all JSX. But jumping from CSS and declarative animations on web to React Native's Animated.spring and StyleSheet.create is just a real big pain, and requires a lot of learning.

This is a big problem for small teams - vastly different platforms means developers need to learn a lot more to do both, or you need to hire more developers on separate web and mobile teams.

But this problem is finally solved ğŸŽ‰

<motion.div
  className="p-4 bg-gray-800 rounded-lg text-white"
  animate={{ x: value * 100 }}
>
  React component
</motion.div>

<Motion.View
  className="p-4 bg-gray-800 rounded-lg"
  animate={{ x: value * 100 }}
>
  <Text className="text-white">
    React Native component
  </Text>
</MotionView>
React component
React Native component
value:
0

Using NativeWind and Legend-Motion we can now write React and React Native code using the same styling and animation patterns, and even mix them together in React Native Web.

In this example you can see an HTML element in React right next to a React Native element in React Native Web, styled and animated in the same way 🤯.

The Problem

React and React Native are similar but different in significant ways, so although React components and React Native components share the same concepts, they need to be written fundamentally differently because:

  1. Styling is different: React uses CSS and React Native uses StyleSheet.
  2. Animations are different: React uses CSS transitions or libraries like Framer Motion while React Native uses the built-in Animated or Reanimated.
  3. Navigation is different: Web and mobile apps are just fundamentally different, so (for now) we're fine with having separate navigation systems and we're focusing on the components themselves. Though, Solito is an interesting project trying to align them that we're watching closely.

The Solution

React developers have recently aligned around using Tailwind CSS for styling and Framer Motion for animations. These both have great DX that feels in many ways easier than the built-in React Native solutions. So if we use libraries for React Native that bring our favorite APIs to React Native, then we can have the same developer nirvana on both platforms.

1. Styling with nativewind

NativeWind is a new library that uses Tailwind CSS as a universal design system for all React Native platforms. It has three features that are crucial for us:

  1. It uses className as a string, just like React components.
  2. It has great performance because it converts className to styles with a Babel plugin so there is almost no runtime cost.
  3. In React Native Web it simply passes className straight through to the DOM components, so it uses normal Tailwind CSS with no overhead.

This means we can have convenient and familiar Tailwind CSS usage on mobile with no overhead, and on React Native Web it just uses Tailwind CSS directly. If you inspect the example below in your browser developer tools you'll see the classNames passed through to the rendered div element.

import { Pressable, Text } from "react-native";

/**
 * A button that changes color when hovered or pressed
 * The text will change font weight when the Pressable is pressed
 */
export function MyFancyButton(props) {
  return (
    <Pressable
        className="p-4 rounded-xl component bg-violet-500 hover:bg-violet-600 active:bg-violet-700"
    >
        <Text
            className="font-bold component-active:font-extrabold"
            {...props}
        />
    </Pressable>
  );
}
Text

2. Animations with Legend-Motion

Legend-Motion is a new library (that we built) to bring the API of Framer Motion to React Native, with no dependencies by using the built-in Animated. This lets us create animations declaratively with an animate prop, and the animation will update automatically whenever the value in the prop changes.

Try hovering over and clicking the box to see it spring around.

<Motion.View
    initial={{ y: -50 }}
    animate={{ x: value * 100, y: 0 }}
    whileHover={{ scale: 1.2 }}
    whileTap={{ y: 20 }}
    transition={{ type: "spring" }}
/>
value:
0

3. Mix React and React Native Web

React Native Web supports mixing HTML and React Native elements together, so it's easy to incrementally drop React Native Web components into a React app. That's a huge boon because we can drop our React Native components into our web apps without needing to write the whole thing with React Native Web.

<motion.div
    className="p-5 text-xs text-black bg-blue-200 rounded-lg"
    whileHover={{ scale: 1.1 }}
    transition={{ type: 'spring' }}
>
    <div>DIV element</div>
    <Motion.View
        className="p-5 mt-6 bg-blue-400 rounded-lg"
        whileHover={{ scale: 1.1 }}
        transition={{ type: 'spring' }}
    >
        <Motion.Text className="text-black">
            React Native Element
        </Motion.Text>
        <motion.div
            className="p-5 mt-6 bg-blue-600 rounded-lg"
            whileHover={{ scale: 1.1 }}
            transition={{ type: 'spring' }}
        >
            DIV text
        </motion.div>
    </Motion.View>
</motion.div>
DIV element
React Native Element
DIV text

Putting it all together

Using NativeWind and Legend-Motion together we can build complex React Native components in an easy declarative way that will look very familiar to React developers:

<Motion.View
  className="p-4 font-bold bg-gray-800 rounded-lg"
  animate={{ x: value * 50 }}
>
  <Text>
    Animating View
  </Text>
</Motion.View>
<Motion.View
  className="p-4 font-bold bg-gray-800 rounded-lg"
  whileHover={{ scale: 1.1 }}
  whileTap={{ x: 30 }}
>
  <Text>
    Press me
  </Text>
</Motion.View>
Animating View
Press me
value:
0

Try it now

1. Legend-Motion

Legend-Motion has no dependencies so it's easy to install.

npm
yarn
npm i @legendapp/motion

Then using it is easy:

import { Motion } from "@legendapp/motion"

<Motion.View
    animate={{
        x: value * 100,
        opacity: value ? 1 : 0.5,
        scale: value ? 1 : 0.7
    }}
>
    <Text>Animating View</Text>
</Motion.View>
Animating View
value:
0

See the docs for more details and advanced usage.

2. nativewind

NativeWind has some tailwindcss configuration and a babel plugin so see its docs to get started.

3. React Native Web

React Native Web support for this is right on the bleeding edge. The pre-release version of React Native Web 0.18 adds the style extraction features that NativeWind depends on. But there's an issue in its implementation of Animated that breaks all other styles when using using extracted styles. I have a fork of the pre-release 0.18 version that fixes this, so if you want to try it now, install react-native-web from my fork:

npm
yarn
npm i https://gitpkg.now.sh/jmeistrich/react-native-web/packages/react-native-web?0.18-animated

It's a pre-release version so of course be careful when using it in production, but we're using it for the examples on this site and another app in development and haven't found any issues. Hopefully RNW 0.18 will release soon with Animated working well, and we can stop using my fork ğŸ¤ž.

Towards Developer Utopia 🌟☀️✨🌈

To get to the utopic future of one platform that runs everywhere there's basically three paths:

  1. All web: This has long been the only viable solution, but mobile web apps can be slow and clunky if not done right. It is possible to build great mobile web apps, and we'll have a future blog post on that, but it's hard.
  2. All React Native: React Native Web is getting there! But it still needs to progress further, and we hope it does! See The Case for the React Native Web Singularity for a deeper dive. For now, it has a performance overhead compared to normal web apps and doesn't support all web features yet.

The problem with both of those solutions is you have to go all in on one platform and accept the limitations. We prefer what I like to call:

  1. A Pleasant Mix 🥰: We use React Native for mobile apps where it shines. We use React for web apps where it shines, and we drop in React Native Web components when we want to share components. For example, we have an admin dashboard in React with a Preview button that embeds the actual React Native components users see in the mobile apps.

Now that we finally can use the same styling and animation patterns, it's much easier for one team to work on both React and React Native. Until recently we had separate web and mobile teams, but in just the past few weeks that we've been using NativeWind and Legend-Motion, we've already merged everyone into one team that can do anything 🚀.

Stay tuned

We are very excited about this new world of React and React Native working beautifully together! And we hope you are too ğŸŽ‰ğŸ¥³.

A huge shoutout to NativeWind for being so great. Give it a star on Github and follow Mark Lawlor for updates.

Legend-Motion is our first open source library and we plan to keep improving it. We're also working on pulling out more of our core code into open source projects, so keep an eye on this blog and follow us or me on Twitter for updates: @LegendAppHQ or @jmeistrich.