After a quick review of TypeScript handbook I started reading React Native documentation. I hope my notes will be useful for other SwiftUI devs who want to try RN.

@ViewBulilder -> JSX

JSX is an embeddable XML-like syntax that allows developers to describe UI in a declarative manner, just like view builders in SwiftUI. TypeScript files that contain JSX must have .tsx extension.

To “escape back” into TypeScript world use curly braces.

export default function Hello() {
    const name = "John";
    return (
      <Text>Hello, {name}!</Text>
    );
}

You can only use curly braces in two ways inside JSX: 1 As text directly inside a JSX tag, as in the example above. 2 As attributes immediately following the = sign: src={avatar} will read the avatar variable, but src="{avatar}" will pass the string "{avatar}".

In addition to primitive types and simple expressions, you can pass objects in JSX. Objects are also denoted with curly braces, like { name: "John Johnson", posts: 5 }. So, to pass a TS object into JSX, you must wrap the object in another pair of curly braces: person={ { name: "John Johnson", posts: 5 }}. You will see this with inline CSS styles in JSX.

Conditional rendering

  1. You can use if to assign conditional content to a variable and later use it inside JSX block:
if (isLoggedIn) {
  content = <AdminPanel />;
} else {
  content = <LoginForm />;
}
return (
  <Container>
    {content}
  </Container>
);
  1. Or, you can use the conditional ? operator.
<Container>
  {isLoggedIn ? (
    <AdminPanel />
  ) : (
    <LoginForm />
  )}
</Container>

You can’t use if inside JSX tag like you do in SwiftUI’s ViewBuilder.

Responding to Events

You can respond to events by declaring event handler functions inside your components:

function MyButton() {
  function handleTap() {
    alert('You tapped me!');
  }

  return (
    <Button onPress={handleTap}>
      Tap me
    </Button>
  );
}

Notice how onClick={handleTap} has no parentheses at the end! We do not want call the event handler function, only to pass it down, otherwise function would be called on each re-render. If you want to define your event handler inline, wrap it in an anonymous function like so: <MyButton onPress={() => alert('You tapped me!')}>

@State

To add state to your component import useState from React, and then declare a state variable inside your component:

import { useState } from 'react';
function MyButton() {
  const [count, setCount] = useState(0); 
  // …
}

You’ll get two things from useState: the current state (count), and the function that lets you update it (setCount). While you can choose any names, the conventional format is [variable, setVariable]. Initially, count will be 0 since you initialized it with useState(0). Just like with @State this state is tied to a component instance, so if you declare multiple instances of the same component each of them will have its own state. Keep multiple state variables if their states are unrelated, but if two variables frequently change together, consider merging them into a single object. To update the state, simply call the setter function: function handleClick() {setCount(count + 1);}.

You can’t use regular variables to represent state for two reasons: 1. Local variables don’t persist between renders. When React renders this component a second time, it renders it from scratch - it doesn’t consider any changes to the local variables. 2. Changes to local variables won’t trigger renders. React doesn’t realise it needs to render the component again with the new data.

A Quick Word About Hooks

Functions starting with use (like useState above) are called Hooks. useState is a built-in Hook provided by React. You can find other built-in Hooks in the API reference. You can also write your own Hooks by combining the existing ones. Comparing to other functions hooks have several restrictions. Hooks can only be called at the top level of your components or your own Hooks. You can’t call Hooks inside conditions, loops, or other nested functions. Hooks are functions, but it’s helpful to think of them as unconditional declarations. You “use” React features at the top of your component similar to how you “import” modules at the top of your file. If you want to use useState in a condition or a loop, extract a new component and put it there.

@Binding

In React Native, the closest equivalent to SwiftUI’s @Binding is passing props and using callback functions, but it’s not as seamless as SwiftUI’s two-way binding: <ChildComponent count={count} onPress={handlePress} />

The information you pass down like this is called props. In the child component read props and use them to implement child functionality:

function ChildComponent({ count, onPress }) {
  return (
    <Button 
        title={`${count}`} 
        onPress={() => {
          // Must use the passed setter function
          onCountChange(prevCount => prevCount + 1)
      }}
    />
  );
}

Props can have default values: function Avatar({ user, size = 100 }).

For more complex scenarios, you might use:

  • Props drilling (passing props through multiple layers)
  • Context API (similar to SwiftUI’s @Environment, see example below)
  • Dedicated state management libraries

@Observable / @ObservableObject -> useReducer

useReducer is very different from observation mechnisms in SwiftUI and probably closer to TCA, but this is probably the closest thing React has to offer. Instead of updating properties of an object that describes state of the app you will need to write a function that take current state and an “action” that says how state should be updated. This function called reducer and there is a lot of information available on this topic, so I won’t go in to details.

The following example should be enough for general understanding of how it works.

function counterReducer(state, action) {
    switch (action.type) {
        case 'INCREMENT':
            return { count: state.count + 1 };
        case 'DECREMENT':
            return { count: state.count - 1 };
        default:
            return state;
    }
}

function CounterComponent() {
    const [state, dispatch] = useReducer(counterReducer, { count: 0 });
    return (
        <View>
            <Text>{state.count}</Text>
            <Button title="Increment" onPress={() => dispatch({ type: 'INCREMENT' })} />
        </View>
    );
}

Official documentation has a very detailed explanation with usage examples.

.onAppear & .onChange -> useEffect

Syntax and implementation differ, but the concept is similar: declaratively handle lifecycle and state-dependent operations without mixing them directly into the rendering logic.

function UserProfile() {
  const [userId, setUserId] = useState(null);
  const [userData, setUserData] = useState(null);

  // Similar to .onAppear - runs when component first mounts
  useEffect(() => {
    // Initial data fetch when component appears
    fetchUserProfile(userId);
  }, []); // Empty dependency array means "run only on mount"

  // Similar to .onChange - runs when userId changes
  useEffect(() => {
    if (userId) {
      // Fetch user data whenever userId changes
      fetchUserProfile(userId);
    }
  }, [userId]); // Dependency array specifies which values trigger the effect

  return (
    <View>
      <Text>{userData?.name}</Text>
    </View>
  );
}

Warning: The main purpose of Effects is to allow components connect to and synchronize with external systems. Don’t use Effects to orchestrate the data flow of your application. See you might not need an Effect for details.

Other considerations

Design your components as pure functions that don’t cause side effects. In React, side effects usually belong inside event handlers. Event handlers are functions that React runs when you perform some action—for example, when you click a button. Even though event handlers are defined inside your component, they don’t run during rendering! So event handlers don’t need to be pure. In React there are three kinds of inputs that you can read while rendering: props, state, and context. You should always treat these inputs as read-only.