Jeffrey Hicks

Jeffrey Hicks

Platform Eng @R360

React to SwiftUI Concepts Mapping

Comprehensive mapping table and code comparisons for developers transitioning from React to SwiftUI

By Jeffrey Hicks • Aug 20, 2025 • compiled-notes

Comprehensive Concept Mapping Table

React ConceptSwiftUI EquivalentDescription
ComponentsViewsBasic building blocks of UI
Function ComponentsView structsReusable UI elements
Class ComponentsObservableObject + ViewStateful components with lifecycle
JSXSwiftUI DSLDeclarative syntax for UI
PropsView parameters/propertiesData passed to components
useState@StateLocal component state
useEffectonAppear/onChange/taskSide effects and lifecycle
useContext@Environment/@EnvironmentObjectSharing data across components
useReducer@State + enum actionsComplex state management
useMemoComputed propertiesMemoized calculations
useCallbackFunction propertiesMemoized callbacks
useRef@State/BindingMutable references
Fragment (<>)Group/EmptyViewGrouping without wrapper
map() for listsForEachRendering lists
key propid parameterList item identification
conditional renderingif/else, @ViewBuilderConditional UI
children prop@ViewBuilder contentComposing components
defaultPropsDefault parameter valuesDefault values
PropTypesSwift type systemType checking
Context APIEnvironment valuesGlobal state management
Redux/MobX@StateObject/@ObservedObjectExternal state management
styled-componentsView modifiersStyling components
CSS classesViewModifier protocolReusable styles
onClickonTapGestureClick/tap handling
onChangeonChange modifierInput change handling
Event handlersGesture modifiersUser interactions
Lifecycle methodsView lifecycle modifiersComponent lifecycle
componentDidMountonAppearAfter component renders
componentWillUnmountonDisappearBefore component unmounts
componentDidUpdateonChangeAfter state updates
React.memoEquatable protocolPerformance optimization
Portaloverlay/sheet/fullScreenCoverRendering outside parent
Error BoundariesNot available (use Result)Error handling
SuspenseNot available (use async)Async component loading
Custom HooksCustom View extensionsReusable logic
forwardRefBindingPassing refs to children
Controlled componentsBindingTwo-way data binding
Uncontrolled components@State with onChangeOne-way data flow

Detailed Code Comparisons

Basic Component/View Structure

React Component:

// Function Component
function Welcome(props) {
  return <h1>Hello, {props.name}!</h1>;
}

// With TypeScript
interface WelcomeProps {
  name: string;
}

const Welcome: React.FC<WelcomeProps> = ({ name }) => {
  return <h1>Hello, {name}!</h1>;
};

SwiftUI View:

// SwiftUI View
struct Welcome: View {
    let name: String
    
    var body: some View {
        Text("Hello, \(name)!")
    }
}

Props and Data Passing

React Props:

// Parent Component
function App() {
  const user = { name: "John", age: 30 };
  return <UserProfile user={user} />;
}

// Child Component
function UserProfile({ user }) {
  return (
    <div>
      <h2>{user.name}</h2>
      <p>Age: {user.age}</p>
    </div>
  );
}

SwiftUI Properties:

// Parent View
struct ContentView: View {
    let user = User(name: "John", age: 30)
    
    var body: some View {
        UserProfile(user: user)
    }
}

// Child View
struct UserProfile: View {
    let user: User
    
    var body: some View {
        VStack {
            Text(user.name)
                .font(.title2)
            Text("Age: \(user.age)")
        }
    }
}

State Management

React useState:

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}

SwiftUI @State:

import SwiftUI

struct Counter: View {
    @State private var count = 0
    
    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") {
                count += 1
            }
        }
    }
}

Lists and Iteration

React List Rendering:

function TodoList() {
  const todos = [
    { id: 1, text: "Learn React", completed: false },
    { id: 2, text: "Build an app", completed: true }
  ];
  
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <span style={{ 
            textDecoration: todo.completed ? 'line-through' : 'none' 
          }}>
            {todo.text}
          </span>
        </li>
      ))}
    </ul>
  );
}

SwiftUI ForEach:

struct TodoList: View {
    let todos = [
        Todo(id: 1, text: "Learn SwiftUI", completed: false),
        Todo(id: 2, text: "Build an app", completed: true)
    ]
    
    var body: some View {
        List {
            ForEach(todos, id: \.id) { todo in
                Text(todo.text)
                    .strikethrough(todo.completed)
                    .foregroundColor(todo.completed ? .gray : .primary)
            }
        }
    }
}

Conditional Rendering

React Conditional Logic:

function UserGreeting({ isLoggedIn, username }) {
  if (!isLoggedIn) {
    return <p>Please log in</p>;
  }
  
  return (
    <div>
      {isLoggedIn ? (
        <h1>Welcome back, {username}!</h1>
      ) : (
        <h1>Welcome, Guest!</h1>
      )}
    </div>
  );
}

SwiftUI Conditional Views:

struct UserGreeting: View {
    let isLoggedIn: Bool
    let username: String
    
    var body: some View {
        Group {
            if !isLoggedIn {
                Text("Please log in")
            } else {
                VStack {
                    if isLoggedIn {
                        Text("Welcome back, \(username)!")
                    } else {
                        Text("Welcome, Guest!")
                    }
                }
            }
        }
    }
}

Two-Way Data Binding

React Controlled Components:

function App() {
  const [message, setMessage] = useState("");
  
  return (
    <div>
      <input 
        value={message}
        onChange={(e) => setMessage(e.target.value)}
      />
      <Child message={message} setMessage={setMessage} />
    </div>
  );
}

function Child({ message, setMessage }) {
  return (
    <button onClick={() => setMessage("Hello from child")}>
      Update: {message}
    </button>
  );
}

SwiftUI @Binding:

struct ContentView: View {
    @State private var message = ""
    
    var body: some View {
        VStack {
            TextField("Enter message", text: $message)
            ChildView(message: $message)
        }
    }
}

struct ChildView: View {
    @Binding var message: String
    
    var body: some View {
        Button("Update: \(message)") {
            message = "Hello from child"
        }
    }
}

Context/Environment for Global State

React Context:

// Create Context
const ThemeContext = React.createContext();

// Provider Component
function App() {
  const [theme, setTheme] = useState('light');
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Header />
      <MainContent />
    </ThemeContext.Provider>
  );
}

// Consumer Component
function Header() {
  const { theme, setTheme } = useContext(ThemeContext);
  
  return (
    <header style={{ 
      backgroundColor: theme === 'dark' ? '#333' : '#fff',
      color: theme === 'dark' ? '#fff' : '#333'
    }}>
      <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
        Toggle Theme
      </button>
    </header>
  );
}

SwiftUI Environment:

// Environment Key
struct ThemeKey: EnvironmentKey {
    static let defaultValue: String = "light"
}

extension EnvironmentValues {
    var theme: String {
        get { self[ThemeKey.self] }
        set { self[ThemeKey.self] = newValue }
    }
}

// Root View
struct ContentView: View {
    @State private var theme = "light"
    
    var body: some View {
        VStack {
            HeaderView()
            MainContentView()
        }
        .environment(\.theme, theme)
        .environmentObject(ThemeManager(theme: theme))
    }
}

// Consumer View
struct HeaderView: View {
    @Environment(\.theme) var theme
    
    var body: some View {
        HStack {
            Text("Header")
            Button("Toggle Theme") {
                // Theme switching logic
            }
        }
        .padding()
        .background(theme == "dark" ? Color.black : Color.white)
        .foregroundColor(theme == "dark" ? Color.white : Color.black)
    }
}

Effects and Lifecycle

React useEffect:

function DataComponent() {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    fetchData().then(setData);
    
    return () => {
      // Cleanup
    };
  }, []);
  
  return <div>{data ? data.title : 'Loading...'}</div>;
}

SwiftUI onAppear/task:

struct DataView: View {
    @State private var data: DataModel?
    
    var body: some View {
        Text(data?.title ?? "Loading...")
            .onAppear {
                Task {
                    data = await fetchData()
                }
            }
            .onDisappear {
                // Cleanup
            }
    }
}

Event Handling

React Event Handling:

function Button({ onClick, children }) {
  return (
    <button 
      onClick={onClick}
      onMouseEnter={() => console.log('hover')}
    >
      {children}
    </button>
  );
}

SwiftUI Gesture Handling:

struct CustomButton: View {
    let action: () -> Void
    let content: () -> Content
    
    var body: some View {
        content()
            .onTapGesture {
                action()
            }
            .onHover { isHovered in
                print("hover: \(isHovered)")
            }
    }
}

Two-Way Data Binding

React Controlled Input:

function TextInput({ value, onChange }) {
  return (
    <input 
      type="text"
      value={value}
      onChange={(e) => onChange(e.target.value)}
    />
  );
}

function Parent() {
  const [text, setText] = useState("");
  return <TextInput value={text} onChange={setText} />;
}

SwiftUI Binding:

struct CustomTextField: View {
    @Binding var text: String
    
    var body: some View {
        TextField("Enter text", text: $text)
    }
}

struct Parent: View {
    @State private var text = ""
    
    var body: some View {
        CustomTextField(text: $text)
    }
}

Custom Hooks vs Custom View Modifiers

React Custom Hook:

// Custom Hook
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      return initialValue;
    }
  });
  
  const setValue = (value) => {
    try {
      setStoredValue(value);
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error(error);
    }
  };
  
  return [storedValue, setValue];
}

// Using the hook
function Settings() {
  const [name, setName] = useLocalStorage('name', '');
  
  return (
    <input
      value={name}
      onChange={(e) => setName(e.target.value)}
      placeholder="Enter your name"
    />
  );
}

SwiftUI Custom ViewModifier:

// Custom ViewModifier
struct UserDefaultsStorage: ViewModifier {
    let key: String
    @Binding var value: String
    
    func body(content: Content) -> some View {
        content
            .onAppear {
                value = UserDefaults.standard.string(forKey: key) ?? ""
            }
            .onChange(of: value) { newValue in
                UserDefaults.standard.set(newValue, forKey: key)
            }
    }
}

extension View {
    func userDefaultsStorage(key: String, value: Binding<String>) -> some View {
        self.modifier(UserDefaultsStorage(key: key, value: value))
    }
}

// Using the modifier
struct SettingsView: View {
    @State private var name = ""
    
    var body: some View {
        TextField("Enter your name", text: $name)
            .userDefaultsStorage(key: "name", value: $name)
    }
}

Event Handling

React Event Handlers:

function InteractiveButton() {
  const [pressed, setPressed] = useState(false);
  
  const handleMouseDown = () => setPressed(true);
  const handleMouseUp = () => setPressed(false);
  const handleClick = () => console.log('Button clicked!');
  
  return (
    <button
      onClick={handleClick}
      onMouseDown={handleMouseDown}
      onMouseUp={handleMouseUp}
      style={{
        backgroundColor: pressed ? '#007bff' : '#0056b3',
        transform: pressed ? 'scale(0.95)' : 'scale(1)'
      }}
    >
      Click Me
    </button>
  );
}

SwiftUI Gesture Modifiers:

struct InteractiveButton: View {
    @State private var pressed = false
    
    var body: some View {
        Button("Click Me") {
            print("Button clicked!")
        }
        .scaleEffect(pressed ? 0.95 : 1.0)
        .background(pressed ? Color.blue : Color.blue.opacity(0.8))
        .foregroundColor(.white)
        .cornerRadius(8)
        .onLongPressGesture(minimumDuration: 0, maximumDistance: .infinity, pressing: { pressing in
            withAnimation(.easeInOut(duration: 0.1)) {
                pressed = pressing
            }
        }) {
            // Long press action
        }
        .onTapGesture {
            print("Tap gesture detected")
        }
    }
}

Component Composition

React Component Composition:

// Higher-Order Component pattern
function withLoading(WrappedComponent) {
  return function LoadingComponent({ isLoading, ...props }) {
    if (isLoading) {
      return <div>Loading...</div>;
    }
    return <WrappedComponent {...props} />;
  };
}

// Render props pattern
function DataFetcher({ render }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    fetchData().then(result => {
      setData(result);
      setLoading(false);
    });
  }, []);
  
  return render({ data, loading });
}

// Usage
function App() {
  return (
    <DataFetcher
      render={({ data, loading }) => (
        loading ? <div>Loading...</div> : <div>{data}</div>
      )}
    />
  );
}

SwiftUI View Composition:

// Generic wrapper view
struct LoadingWrapper<Content: View>: View {
    let isLoading: Bool
    let content: () -> Content
    
    var body: some View {
        Group {
            if isLoading {
                ProgressView("Loading...")
            } else {
                content()
            }
        }
    }
}

// ViewBuilder pattern
struct DataFetcher<Content: View>: View {
    @State private var data: String?
    @State private var isLoading = true
    
    let content: (String?, Bool) -> Content
    
    var body: some View {
        content(data, isLoading)
            .onAppear {
                fetchData()
            }
    }
    
    private func fetchData() {
        // Simulate API call
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            data = "Fetched data"
            isLoading = false
        }
    }
}

// Usage
struct ContentView: View {
    var body: some View {
        DataFetcher { data, loading in
            LoadingWrapper(isLoading: loading) {
                if let data = data {
                    Text(data)
                } else {
                    Text("No data")
                }
            }
        }
    }
}

Key Differences and Considerations

Type Safety

  • React: Optional with TypeScript, runtime prop validation with PropTypes
  • SwiftUI: Built-in compile-time type checking with Swift’s type system

Performance

  • React: Virtual DOM, reconciliation, requires optimization (memo, useMemo, useCallback)
  • SwiftUI: Direct native view updates, automatic dependency tracking, minimal optimization needed

Styling

  • React: CSS, styled-components, CSS-in-JS libraries
  • SwiftUI: View modifiers, native styling system integrated with platform design systems

Platform Integration

  • React: Web-first, React Native for mobile
  • SwiftUI: Native iOS/macOS/watchOS/tvOS, deeply integrated with Apple ecosystems

Learning Curve

  • React: Steeper learning curve for state management and optimization
  • SwiftUI: More straightforward for developers familiar with declarative patterns, but requires learning Swift

This comprehensive mapping and code comparison demonstrates how React’s component-based architecture translates to SwiftUI’s protocol-oriented approach. Both frameworks embrace declarative UI patterns, making them conceptually similar despite their different implementation languages and target platforms.

The key differences lie in React’s JavaScript flexibility versus SwiftUI’s Swift type safety, React’s broader cross-platform reach versus SwiftUI’s deep Apple ecosystem integration, and React’s mature third-party ecosystem versus SwiftUI’s tightly integrated first-party experience.

References

  1. SwiftUI as a React Developer
    https://blog.maximeheckel.com/posts/swiftui-as-react-developer/

  2. SwiftUI vs React: Comprehensive Comparison
    https://zthh.dev/blogs/swiftui-vs-react-comprehensive-comparison

  3. Learning SwiftUI as a React Developer
    https://www.codelogbook.com/2022/11/23/learning-swiftui-as-a-react-developer

  4. SwiftUI for React Devs GitHub
    https://github.com/unixzii/swiftui-for-react-devs

  5. React vs SwiftUI Comparison
    https://www.jetpackcompose.app/compare-declarative-frameworks/React-vs-SwiftUI

  6. SwiftUI Against React
    https://www.mux.com/blog/swiftui-against-react

  7. React Native vs SwiftUI Discussion
    https://www.reddit.com/r/reactnative/comments/1l47gyi/if_you_are_struggling_to_choose_between_react/

  8. SwiftUI Hooks GitHub
    https://github.com/ra1028/swiftui-hooks

  9. SwiftUI State Management Comparison (YouTube)
    https://www.youtube.com/watch?v=0h2fQOl3bns

  10. Custom Environment Values in SwiftUI
    https://azamsharp.com/2024/10/27/simplifying-list-sorting-in-swiftui-a-guide-to-custom-environment-values.html

  11. From React to SwiftUI
    https://betterprogramming.pub/from-react-to-swiftui-f08c29b7d3b