Comprehensive mapping table and code comparisons for developers transitioning from React to SwiftUI
React Concept | SwiftUI Equivalent | Description |
---|---|---|
Components | Views | Basic building blocks of UI |
Function Components | View structs | Reusable UI elements |
Class Components | ObservableObject + View | Stateful components with lifecycle |
JSX | SwiftUI DSL | Declarative syntax for UI |
Props | View parameters/properties | Data passed to components |
useState | @State | Local component state |
useEffect | onAppear/onChange/task | Side effects and lifecycle |
useContext | @Environment/@EnvironmentObject | Sharing data across components |
useReducer | @State + enum actions | Complex state management |
useMemo | Computed properties | Memoized calculations |
useCallback | Function properties | Memoized callbacks |
useRef | @State/Binding | Mutable references |
Fragment (<>) | Group/EmptyView | Grouping without wrapper |
map() for lists | ForEach | Rendering lists |
key prop | id parameter | List item identification |
conditional rendering | if/else, @ViewBuilder | Conditional UI |
children prop | @ViewBuilder content | Composing components |
defaultProps | Default parameter values | Default values |
PropTypes | Swift type system | Type checking |
Context API | Environment values | Global state management |
Redux/MobX | @StateObject/@ObservedObject | External state management |
styled-components | View modifiers | Styling components |
CSS classes | ViewModifier protocol | Reusable styles |
onClick | onTapGesture | Click/tap handling |
onChange | onChange modifier | Input change handling |
Event handlers | Gesture modifiers | User interactions |
Lifecycle methods | View lifecycle modifiers | Component lifecycle |
componentDidMount | onAppear | After component renders |
componentWillUnmount | onDisappear | Before component unmounts |
componentDidUpdate | onChange | After state updates |
React.memo | Equatable protocol | Performance optimization |
Portal | overlay/sheet/fullScreenCover | Rendering outside parent |
Error Boundaries | Not available (use Result) | Error handling |
Suspense | Not available (use async) | Async component loading |
Custom Hooks | Custom View extensions | Reusable logic |
forwardRef | Binding | Passing refs to children |
Controlled components | Binding | Two-way data binding |
Uncontrolled components | @State with onChange | One-way data flow |
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)!")
}
}
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)")
}
}
}
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
}
}
}
}
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)
}
}
}
}
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!")
}
}
}
}
}
}
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"
}
}
}
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)
}
}
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
}
}
}
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)")
}
}
}
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)
}
}
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)
}
}
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")
}
}
}
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")
}
}
}
}
}
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.
SwiftUI as a React Developer
https://blog.maximeheckel.com/posts/swiftui-as-react-developer/
SwiftUI vs React: Comprehensive Comparison
https://zthh.dev/blogs/swiftui-vs-react-comprehensive-comparison
Learning SwiftUI as a React Developer
https://www.codelogbook.com/2022/11/23/learning-swiftui-as-a-react-developer
SwiftUI for React Devs GitHub
https://github.com/unixzii/swiftui-for-react-devs
React vs SwiftUI Comparison
https://www.jetpackcompose.app/compare-declarative-frameworks/React-vs-SwiftUI
SwiftUI Against React
https://www.mux.com/blog/swiftui-against-react
React Native vs SwiftUI Discussion
https://www.reddit.com/r/reactnative/comments/1l47gyi/if_you_are_struggling_to_choose_between_react/
SwiftUI Hooks GitHub
https://github.com/ra1028/swiftui-hooks
SwiftUI State Management Comparison (YouTube)
https://www.youtube.com/watch?v=0h2fQOl3bns
Custom Environment Values in SwiftUI
https://azamsharp.com/2024/10/27/simplifying-list-sorting-in-swiftui-a-guide-to-custom-environment-values.html
From React to SwiftUI
https://betterprogramming.pub/from-react-to-swiftui-f08c29b7d3b