Skip to content

Instantly share code, notes, and snippets.

@tshego3
Last active February 26, 2026 21:37
Show Gist options
  • Select an option

  • Save tshego3/159bd9cebe9dcb0ae2e48393e0f5fe74 to your computer and use it in GitHub Desktop.

Select an option

Save tshego3/159bd9cebe9dcb0ae2e48393e0f5fe74 to your computer and use it in GitHub Desktop.

Set of basic functionalities from React in one place.

Cheatsheet has been created by https://foreach.pl trainers for training participants.

React and React Native: Build cross-platform JavaScript and TypeScript apps for the web, desktop, and mobile 5th Edition

Check also other Cheatsheets:

Angular

TypeScript

Table of Contents

Basic React commands

Command Notes
npx create-react-app@latest {app name} --template @fluentui/cra-template@latest OR npx create-react-app@latest {app name} --template typescript@latest Create new React app
npm install downloads and sets up all the project's necessary code libraries (dependencies) into a node_modules folder
npm run dev Run a custom command defined in the project's package.json file
npm start Run project locally
npm test run tests in React app
npm eject remove the single build dependency from your project
npm cache clean --force Clean cache
npm uninstall @packageName Uninstall package

JSX

JSX allows us to write HTML elements in JavaScript and place them in the DOM. Basically, it converts HTML tags into react elements.

Injection JS into HTML

 const value = 'Hello world';
 return (<div>{value}</div>);

'Hello world' will be injected and displayed inside <div> element

Element declaration

 const value = 'Hello World';
 const element = <div className="App">{value}</div>;
 return (element);

We can assign specific DOM fragments to a particular variable (element above) and then (re)use it everywhere.

Function injection

 function addNumbers(x1, x2) {
   return x1 + x2;
 }
 const element = <div className="App">{addNumbers(2, 5)}</div>

Not only string values can be injected into DOM. The result of the above case will be div with 7 inside it as addNumbers was injected into an element.

Attributes

  const avatarUrl = "img/picture.jpg"
  const element = <img src={avatarUrl}></img>;

JavaScript code can be also called for DOM element properties/events

Fragments

JSX syntax must have one top parent HTML element. When we don't want to pack our HTML into (for example) one div or other elements then we can use fragment which won't be rendered in our HTML

Example:

<React.Fragment>
   <td>val1</td>
   <td>val2</td>
</React.Fragment>

or

<>
  <td>val1</td>
  <td>val2</td>
</>

References

We can refer to html element from JS

  1. Declare a reference
this.element = React.createRef();
  1. Assign a reference to element
<div ref={this.element}></div>
  1. Use reference to interact with element
this.element.current.focus();

Tip: References works for the class component but not for function component (unless you use useRef hook, see Other hooks )

Spread operator

Spread operator or Spread syntax in JavaScript/TypeScript. It’s a concise way to extend or override parts of an object without rewriting everything. Keeps your theme consistent with the defaults while customizing only what you need.

const base = { a: 1, b: 2 };
const extended = { ...base, b: 99, c: 3 };

console.log(extended);
// { a: 1, b: 99, c: 3 }

Components

"React lets you define components as classes or functions. Components defined as classes currently provide more features"

Function component

Sample component:

function App() {
  return <div>Hello World</div>;
}
export default App;

Sample component with parameters:

function Welcome(props) {
    return <h1>Hello, {props.name}</h1>;
}
<Welcome name="Sara" />

Class component

Sample component:

class App extends React.Component  {
  render() {
    return <div>Hello World</div>;
  }
}
export default App;

Sample component with the parameters:

class Welcome extends React.Component  {
  render() {
    return <div>Hello {this.props.name}</div>;
  }
}
export default App;
<Welcome name="Sara" />

Events

Sample onClick event in an element:

<a href="#" onClick={this.onClick}> Click me </a>

onClick function:

onClick(e) {
    e.preventDefault();
    // some logic here
}

Access to "this" inside event:

There are a few ways to achieve that:

  1. bind this
this.onClick = this.onClick.bind(this);
  1. Arrow function. For arrow function, you don't have to perform binding because arrow functions are performing this binding by themselves by referring to this with _this variable _this = this
onClick = (e) => {
    e.preventDefault();
    console.log(this);
}

or

 <a href="#" onClick={(e) => this.onClick(e)}> Click me</a>

Child event

We can call a function in parent component from the child component.

Child component (somewhere in code):

this.props.onChange(val1, val2,...)

Parent component:

 <Child onChange={hasChanged} />
function hasChanged(val1, val2,...) {
 // some logic here
}

Conditional element

Show element depending on the implemented condition

Option 1:

function SomeElement(props) {
   const show = props.isShowed;
   if(show) {
      return <div>Here is element</div>;
   }         
}

then:

<SomeElement isShowed = {true} />

Option 2:

let element;      
if(this.state.isShown) {
    element  = <div> Here is element </div>;
} 

and then use the element variable in jsx

Option 3:

{this.state.isShow  ?  <div> Here is element </div> : 'No element'}

Option 4:

{this.state.isShow  &&  <div> Here is element </div>}

List of elements

The recommended way to show a list of the same components is to use the "map" function. Example:

const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((numer, index) =>
  <li key={number.toString()}>
    {number}
  </li>
);

Then just use listItems element in your JSX DOM. Please note key property inside - it is obligatory to identify particular row in list.

Content projection

Content projection is injecting some HTML from parent component to child component

Example:

Parent component:

<MyElement><h1>Hello world</h1></MyElement>

Child (MyElement) component:

<div>
   {this.props.children}
</div>

<h1>Hello world</h1> will be injected to place where {this.props.children} has been used, so MyElement will look like:

<div>
   <h1>Hello world</h1>
</div>

Higher order component

Create a parameterized component in order to reuse it in different ways.

Example:

const myHoc = settings => WrappedComponent => {
    return class DecoratingComponent extends React.Component {
        render() {
            return (<div className={settings}><WrappedComponent {...this.props} /></div>)
        }
    }
}

Usage:

const MyWrappedComponent = myHoc('test')(SomeComponent);
<MyWrappedComponent/>

test will be injected into className

Routing

The Router will be described with react-router-dom library usage, which is often recommended.

Install:

npm install react-router-dom 

Sample presenting an area where components matching to current URL will be rendered:

<Router>
   <Switch>
     <Route path="/help"><Help /></Route>
     <Route path="/list"><List /></Route>
     <Route path="/"><Home /></Route>
   </Switch>
 </Router>

Link changing current url:

<Link className="nav-link" to="/list">List</Link>

Tip: Route and Link need to be in same Router element

Nested routing

Parent:

<Route path="/some" component={SomeComponent}></Route>

SomeComponent:

function  SomeComponent({ match }) {
    return (
        <Router>
            <Switch>
                <Route path={`${match.path}/child`}><Child /></Route>
            </Switch>
        </Router>
    )
}

Routing with parameters

<Route path="/some/:id" component={SomeComponent}></Route>

and then in SomeComponent we have access to:

{match.params.id}

Authentication

Good idea is to create a custom Route component that will show specific component only when our authentication logic is passed:

function PrivateRoute({ componentComponent, ...rest }) {
    return (
        <Route {...rest} render={(props) => (_isAuthentictedLogic_) ?
            (<Component {...props} />) :
            (<Redirect to={{ pathname"/noAccess", state{ fromprops.location } }} />
            )}
        />
    );
}

Then instead of Route use PrivateRoute

<PrivateRoute path="/list" component={List}></PrivateRoute>

State and life cycle

State

The state decides about the update to a component’s object. When state changes, the component is re-rendered.

Declaring initial state:

constructor() {
    this.state = { userId: 2, userName: "Jannice"};
}

Using state:

<div> {this.state.userName} </div>

Updating state:

this.setState({ userId: 3, userName: "Tom"});

After setState component will be re-rendered

Life cycle

Life cycle Notes
componentDidMount Called at the beginning of component life - when a component has been rendered
componentWillUnmount This method is called before the unmounting of the component takes place.
componentWillUpdate Before re-rendering component (after change)
componentDidUpdate After re-rendering component (after change)
componentDidCatch Called when an error has been thrown
shouldComponentUpdate Determines whether a component should be updated after a change or not
getDerivedStateFromProps Called at the beginning of component life and when props/state will change
getSnapshotBeforeUpdate Called just before rerendering component - in order to save state

Note Deprecation alert for componentWillUnmount and componentWillUpdate. This can cause memory leaks for server rendering.

Example:

componentDidMount() {
    console.log("componentDidMount")
    // some logic here
}

componentWillUnmount() {
    console.log("componentWillUnmount")
    // some logic here
}

Forms

Sample form:

onValueChange  = (event) => {
    this.setState({ valueevent.target.value  });
}
onSubmit = (e) => {
	e.preventDefault();
	// Submit logic
}
<form onSubmit={this.handleSubmit}>
    <input type="text"
        value={this.state.value}
        onChange={this.onValueChange}
    />
    
    <input type="submit" value="Send" />
</form>

Validation is described here: https://webfellas.tech/#/article/5

Http requests

There are a lot of ways to communicate with our backend API. One of them is to use Axios library.

Install: npm install axios

Example with GET:

axios.get('/user', {
params{ID12345}
}).then(function (response) {
   //some logic here
})
.catch(function (error) {
   //some logic here
})
.finally(function () {
   //some logic here
}); 

Example with POST:

axios.post('/user', {
    firstName'Fred',
    id: 2
}).then(function (response) {
    //some logic here
})
.catch(function (error) {
    //some logic here
});

Example with async call:

const response = await axios.get('/user?ID=12345');

Tests

Useful libs:

npm install --save-dev jest to run tests

npm install --save-dev @testing-library/react set of functions that may be useful for tests in React

Sample tests:

let container = null;
beforeEach(() => {
  container = document.createElement("div");
  document.body.appendChild(container);
});
afterEach(() => {
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});
it("renders menu with Help", () => {
  act(() => {
    render(<Router><SomeComponent /></Router>, container);
  });
  expect(container.textContent).toBe("Some text inside");
});

Interaction

Click on element:

import { fireEvent } from '@testing-library/react'
fireEvent.click(container.querySelector('.some-class));

Change value in input:

fireEvent.change(container.querySelector('.some-class)), {
    target{ value"12345" }
});

Check if element contains class

expect(someEl.classList.contains('disabled')).toBe(false);

Mocking http

npm install axios-mock-adapter --save-dev

Example:

var axios = require('axios');
var MockAdapter = require('axios-mock-adapter');
var mock = new MockAdapter(axios);
mock.onGet('/users').reply(200, {
  users[
    { id1, name'John Smith' }
  ]
});

With specific parameter

mock.onGet('/users', { params{ searchText'John' } }).reply()

Mocking components

Example:

jest.mock("./SomeComponent", () => {
  return function DummyComponent(props) {
    return (
      <div data-testid="square">
        here is square
      </div>
    );
  };
});

Context

Context is a "bag" for some data common for node of components.

Declare context:

import React from 'react';
export const ColorContext = React.createContext('red');

'red' is default value

Create context and distribute it to child component:

const { Provider } = ColorContext;
return (
    <Provider value={this.state.color}>
    	<SomeComponent />
    </Provider>
);

Use it in child component:

const { Consumer } = ColorContext;
return (
   <Consumer>
	{value => <div>{value}</div>}
   </Consumer>
)

Use it in child component outside render function:

static contextType = ColorContext;
let value = this.context.value

Hooks

Hooks let us use in functions React features dedicated previously for only classes, like state.

Basic hooks

useState hook

Can replace state managment

const [count, setCount] = useState(0);

count can be used to read data

0 is the default value for count

setCount(10) is a function to change count state value

useEffect hook

Can replace componentDidMount, componentDidUpdate, componentWillUnmount and other life cycle component events

Instead of using componentDidMount, componentDidUpdate we can use useEffect hook like this:

useEffect(() => {
    document.title = `Clicked ${count}`;
});

In order to act as componentWillUnmount:

useEffect(() => {
    return () => {/*unsubscribe*/};
});

useContext hook

Hooks for context implementation. See Context

Parent:

const CounterContext = React.createContext(counter);

function App() {
  return (
    <CounterContext.Provider value={10}>
      <ChildComponent />
    </CounterContext.Provider>
  );
}

Child:

const counter = useContext(CounterContext);

Other hooks

Hook Usage Notes
useReducer const [state, dispatch] = useReducer(reducer, initialArg, init); Used for redux
useCallback const memoizedCallback = useCallback(() => {doSomething(a, b);},[a, b],); Returns a memoized callback
useMemo const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); Returns a memoized value.
useRef const refContainer = useRef(initialValue); Hooks for element reference
useDebugValue useDebugValue(value) Display information in React DevTools

Custom hook

Custom hooks can be created. Sample implementation:

function useColor(initialColor) {
    const [color, changeColor] = useState(initialColor);
    function change(color) {
        changeColor(color);
    }
    useEffect(() => {
        console.log('Color changed');
    });
    return [color, change];
}

TypeScript in React

Add to project commands

npx create-react-app my-app --typescript - new app with TypeScript

npm install --save typescript @types/node @types/react @types/react-dom @types/jest - add TypeScript to existing project

npx tsc --init - init tsconfig

Add type for custom DOM element

declare namespace JSX {
    interface IntrinsicElements {fooany}
}
<foo />; // ok
<bar />; // error

Interview questions

Q: How can we declare the component??

A: Use class extending React.Component or use function with React.FC type returning JSX.

Q: What is a difference between function component and class component?

A: Components defined as classes currently provide more features. Anyway, hooks can change that.

Q: In what type of component can we use hooks?

A: Function component

Q: How can we inject some HTML from parent component to child component?

A: Using content projection - > this.props.children will keep parent html element. See Content projection

Q: Describe useEffect hook

A: UseEffect can replace componentDidMount, componentDidUpdate, componentWillUnmount and other life cycle component events. See Basic hooks

Q: What is the difference between state and props??

A: The state decides about the update to a component’s object. When state changes, the component is re-rendered. Props are parameters passed from parent component to child component.

Q: What is a higher-order component??

A: Basically its's parameterized component. It allows reusing of a particular component in many ways. Sample implementation:

const myHoc = settings => WrappedComponent => {
    return class DecoratingComponent extends React.Component {
        render() {
            return (<div className={settings}><WrappedComponent {...this.props} /></div>)
        }
    }
}

usage:

const MyWrappedComponent = myHoc('test')(SomeComponent);
<MyWrappedComponent/>

Q: What is JSX?

A: JSX allows us to write HTML elements in JavaScript and place them in the DOM. Basically, it converts HTML tags into react elements. See JSX

Q: How can we create new react app?

A: Using command npx create-react-app {app name}

Q: What will be the result of npm eject?

A: Webpack for React application won't be handled automatically anymore. We will have access to Webpack configuration files in order to customize it to our needs.

React Native Todo App with Expo And TypeScript

Complete Beginner Guide

Development Environment Setup

Prerequisites

# Install Homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Check your version
brew --version
# Uninstall Homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/uninstall.sh)"

# Install Node.js (v18 or newer, this also updates Homebrew and Node.js)
brew install node
# Check your version
node -v && npm -v
# Uninstall Node.js
brew uninstall node
# Cleanup Node.js global packages
npm list -g --depth=0
npm uninstall -g <package-name>  # Replace with actual package name

# Install Watchman (for file watching)
brew install watchman
# Check your version
watchman --version
# Uninstall Watchman
brew uninstall watchman

# Open your .zshrc file in a text editor
open ~/.zshrc # This will open the file in your default text editor (usually TextEdit on macOS)
# Add the following lines at the bottom of the file
export PATH="/opt/homebrew/bin:$PATH" # This ensures the CocoaPods CLI tools like pod are available in your terminal
export PATH=$PATH:~/Library/Android/sdk # This ensures the Android SDK tools like adb are available in your terminal
export PATH=$PATH:~/Library/Android/sdk/platform-tools
export PATH=$PATH:~/Library/Android/sdk/emulator
export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/emulator
export PATH=$PATH:$ANDROID_HOME/platform-tools
export GEM_HOME=$HOME/.gem
export PATH=$GEM_HOME/bin:$PATH # Save and close the file
# Apply the changes
source ~/.zshrc

# Install a newer Ruby version
brew install ruby # brew reinstall ruby
# Open your .zshrc file in a text editor
open ~/.zshrc
# Add this to your .zshrc file
export PATH="/opt/homebrew/opt/ruby/bin:$PATH"
# Apply the changes
source ~/.zshrc
# Check your version
ruby -v

# Install TypeScript
npm install -g typescript # use sudo if required
# Check your version
tsc --version
# Uninstall TypeScript
npm uninstall -g typescript

# Install React Native CLI
npm install -g @react-native-community/cli # use sudo if required
# Check your version
npx @react-native-community/cli --version
# Uninstall React Native CLI
npm uninstall -g @react-native-community/cli

# Install Expo CLI 
npm install -g @expo/cli # use sudo if required
# Check version
npx expo --version
# Uninstall Expo CLI
npm uninstall -g @expo/cli

# For iOS testing on macOS 
brew install cocoapods
# Check version
pod --version # If failed: brew link --overwrite cocoapoda
# Uninstall CocoaPods
brew uninstall cocoapods

# Required if Android Studio is not installed - Install OpenJDK distribution called Azul Zulu using Homebrew. This distribution offers JDKs for both Apple Silicon and Intel Macs.
brew install --cask zulu@17 # brew reinstall zulu@17
# Open your .zshrc file in a text editor
open ~/.zshrc
# Add this to your .zshrc file
export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home
export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/emulator
export PATH=$PATH:$ANDROID_HOME/platform-tools
# Apply the changes
source ~/.zshrc
# Check your version
zulu -v

# Install or update command-line tools (prompts a GUI installer if needed)
xcode-select --install
# Ensure Xcode path is correctly set
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
# Accept Xcode license
sudo xcodebuild -license accept # sudo xcodebuild -license
# Test simctl
xcrun simctl help

# List available iOS simulators and their UDIDs
xcrun simctl list devices
# JSON output (easier to parse programmatically)
xcrun simctl list devices --json
# List available physical devices connected to your Mac and their UDIDs
xcrun xctrace list devices

# List connected Android devices/emulators
adb devices
React Native with a Framework

Create New Expo Project

# expo init TodoApp --template blank-typescript
npx create-expo-app@latest TodoApp --template default # use sudo if required - Default template. Designed to build multi-screen apps. Includes recommended tools such as Expo CLI, Expo Router library and TypeScript configuration enabled. Suitable for most apps.
cd TodoApp
# Run the following command in your project's root directory:
npx expo install expo-dev-client
# Installs the CocoaPods dependencies listed in your Podfile. These are native iOS libraries your app depends on
cd ios
pod install --repo-update --ansi # use sudo and --allow-root if required
# Navigate back to the parent directory
cd ..

Running the Expo App

# Start the development server
npx expo start

# Or run directly on specific platform
npx expo start --ios # npm run ios
npx expo run:ios --device 51F0082E-AC8D-4BD9-AD37-2A7811EA22A8
npx expo start --android # npm run android
npx expo run:android --device emulator-5554
npx expo start --web # npm run web

# When deploying on physical devices use the release configuration, as the debug configuration requires a development server
npx expo run:ios --device 00008140-00062D3122D0801C --configuration Release
    
# Clean cache if errors occur
npm cache clean --force # use sudo if required 
# Clear Expo cache directory
rm -rf ~/.expo

Project Structure (Expo)

TodoApp/
├── app.json               # Expo configuration
├── package.json
├── tsconfig.json
├── App.tsx               # Main app entry point
├── src/
│   ├── components/
│   │   ├── common/
│   │   │   ├── ActivityIndicator.tsx
│   │   │   └── SafeAreaView.tsx
│   │   ├── login/
│   │   │   └── LoginForm.tsx
│   │   ├── todos/
│   │   │   ├── TodoList.tsx
│   │   │   └── TodoDetail.tsx
│   │   └── navigation/
│   │       └── AppNavigator.tsx
│   ├── models/
│   │   ├── TodoItem.ts
│   │   ├── User.ts
│   │   └── TokenResponse.ts
│   ├── services/
│   │   └── TodoService.ts
│   ├── hooks/
│   │   ├── useAuth.ts
│   │   └── useTodos.ts
│   ├── context/
│   │   └── AuthContext.tsx
│   ├── utils/
│   │   ├── storage.ts
│   │   └── converters.ts
│   └── screens/
│       ├── LoginScreen.tsx
│       ├── TodoListScreen.tsx
│       └── TodoDetailScreen.tsx
└── assets/               # Images, fonts, etc.

File-by-File Implementation (Expo Version)

Package.json (Expo Dependencies)

{
  "name": "todoapp",
  "version": "1.0.0",
  "scripts": {
    "start": "expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web"
  },
  "dependencies": {
    "expo": "~49.0.0",
    "expo-status-bar": "~1.6.0",
    "react": "18.2.0",
    "react-native": "0.72.0",
    "@expo/vector-icons": "^13.0.0",
    "expo-secure-store": "~12.0.0",
    "@react-navigation/native": "^6.1.0",
    "@react-navigation/native-stack": "^6.9.0",
    "react-native-screens": "~3.22.0",
    "react-native-safe-area-context": "4.6.3"
  },
  "devDependencies": {
    "@babel/core": "^7.20.0",
    "@types/react": "~18.2.0",
    "typescript": "^5.1.0"
  }
}

Expo Configuration

// app.json
{
  "expo": {
    "name": "TodoApp",
    "slug": "todoapp",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "userInterfaceStyle": "light",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "assetBundlePatterns": [
      "**/*"
    ],
    "ios": {
      "supportsTablet": true
    },
    "android": {
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#FFFFFF"
      }
    },
    "web": {
      "favicon": "./assets/favicon.png"
    },
    "plugins": [
      "expo-secure-store"
    ]
  }
}

TypeScript Models

// src/models/TokenResponse.ts
export interface TokenResponse {
  token_type?: string;
  expires_in: number;
  ext_expires_in: number;
  access_token?: string;
}
// src/models/TodoItem.ts
export interface TodoItem {
  id: number;
  userId: number;
  title: string;
  completed: boolean;
}
// src/models/User.ts
export interface User {
  id: number;
  username: string;
  password: string;
  email: string;
}

export interface UserResponse {
  users: User[];
}

Storage Utility (Using Expo Secure Store)

// src/utils/storage.ts
import * as SecureStore from 'expo-secure-store';

export const Storage = {
  // Username
  getUsername: async (): Promise<string> => {
    return (await SecureStore.getItemAsync('username')) || '';
  },
  setUsername: async (username: string): Promise<void> => {
    await SecureStore.setItemAsync('username', username);
  },

  // Password
  getPassword: async (): Promise<string> => {
    return (await SecureStore.getItemAsync('password')) || '';
  },
  setPassword: async (password: string): Promise<void> => {
    await SecureStore.setItemAsync('password', password);
  },

  // Access Token
  getAccessToken: async (): Promise<string> => {
    return (await SecureStore.getItemAsync('accessToken')) || '';
  },
  setAccessToken: async (token: string): Promise<void> => {
    await SecureStore.setItemAsync('accessToken', token);
  },

  // Token Expiration
  getAccessTokenExpiration: async (): Promise<Date> => {
    const expiration = await SecureStore.getItemAsync('accessTokenExpiration');
    return expiration ? new Date(expiration) : new Date();
  },
  setAccessTokenExpiration: async (expiration: Date): Promise<void> => {
    await SecureStore.setItemAsync('accessTokenExpiration', expiration.toISOString());
  },

  // Clear all storage (logout)
  clearAll: async (): Promise<void> => {
    await SecureStore.deleteItemAsync('username');
    await SecureStore.deleteItemAsync('password');
    await SecureStore.deleteItemAsync('accessToken');
    await SecureStore.deleteItemAsync('accessTokenExpiration');
  },
};

Todo Service (HTTP Operations)

// src/services/TodoService.ts
import { TodoItem, UserResponse } from '../models';

export class TodoService {
  private baseUrl = 'https://jsonplaceholder.typicode.com';
  private dummyJsonUrl = 'https://dummyjson.com';

  async login(username: string, password: string): Promise<boolean> {
    try {
      const response = await fetch(`${this.dummyJsonUrl}/users`);
      const userResponse: UserResponse = await response.json();

      if (userResponse.users) {
        const user = userResponse.users.find(
          u => u.username.toLowerCase() === username.toLowerCase() && u.password === password
        );
        return user !== undefined;
      }
    } catch (error) {
      console.error('Login failed:', error);
    }

    return false;
  }

  async getTodos(): Promise<TodoItem[]> {
    try {
      const response = await fetch(`${this.baseUrl}/todos`);
      return await response.json();
    } catch (error) {
      console.error('Failed to fetch todos:', error);
      return [];
    }
  }

  async getTodo(id: number): Promise<TodoItem | null> {
    try {
      const response = await fetch(`${this.baseUrl}/todos/${id}`);
      return await response.json();
    } catch (error) {
      console.error('Failed to fetch todo:', error);
      return null;
    }
  }

  async createTodo(todo: Omit<TodoItem, 'id'>): Promise<TodoItem | null> {
    try {
      const response = await fetch(`${this.baseUrl}/todos`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(todo),
      });
      return await response.json();
    } catch (error) {
      console.error('Failed to create todo:', error);
      return null;
    }
  }

  async updateTodo(id: number, todo: Partial<TodoItem>): Promise<TodoItem | null> {
    try {
      const response = await fetch(`${this.baseUrl}/todos/${id}`, {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(todo),
      });
      return await response.json();
    } catch (error) {
      console.error('Failed to update todo:', error);
      return null;
    }
  }

  async deleteTodo(id: number): Promise<boolean> {
    try {
      const response = await fetch(`${this.baseUrl}/todos/${id}`, {
        method: 'DELETE',
      });
      return response.ok;
    } catch (error) {
      console.error('Failed to delete todo:', error);
      return false;
    }
  }
}

Custom Hooks

// src/hooks/useAuth.ts
import { useState, useCallback } from 'react';
import { TodoService } from '../services/TodoService';
import { Storage } from '../utils/storage';

export const useAuth = () => {
  const [isLoading, setIsLoading] = useState(false);
  const todoService = new TodoService();

  const login = useCallback(async (username: string, password: string): Promise<boolean> => {
    if (!username || !password) {
      return false;
    }

    setIsLoading(true);
    try {
      const isAuthenticated = await todoService.login(username, password);
      
      if (isAuthenticated) {
        await Storage.setUsername(username);
        await Storage.setPassword(password);
        await Storage.setAccessToken('expo-mock-token');
        await Storage.setAccessTokenExpiration(new Date(Date.now() + 3600000));
      }
      
      return isAuthenticated;
    } catch (error) {
      console.error('Login error:', error);
      return false;
    } finally {
      setIsLoading(false);
    }
  }, []);

  const logout = useCallback(async (): Promise<void> => {
    await Storage.clearAll();
  }, []);

  return {
    isLoading,
    login,
    logout,
  };
};
// src/hooks/useTodos.ts
import { useState, useCallback, useEffect } from 'react';
import { TodoItem } from '../models';
import { TodoService } from '../services/TodoService';

export const useTodos = () => {
  const [todos, setTodos] = useState<TodoItem[]>([]);
  const [isLoading, setIsLoading] = useState(false);
  const todoService = new TodoService();

  const loadTodos = useCallback(async () => {
    setIsLoading(true);
    try {
      const fetchedTodos = await todoService.getTodos();
      setTodos(fetchedTodos);
    } catch (error) {
      console.error('Failed to load todos:', error);
    } finally {
      setIsLoading(false);
    }
  }, []);

  const getTodo = useCallback(async (id: number): Promise<TodoItem | null> => {
    return await todoService.getTodo(id);
  }, []);

  const createTodo = useCallback(async (todo: Omit<TodoItem, 'id'>): Promise<void> => {
    const newTodo = await todoService.createTodo(todo);
    if (newTodo) {
      setTodos(prev => [...prev, newTodo]);
    }
  }, []);

  const updateTodo = useCallback(async (id: number, todo: Partial<TodoItem>): Promise<void> => {
    const updatedTodo = await todoService.updateTodo(id, todo);
    if (updatedTodo) {
      setTodos(prev => prev.map(t => t.id === id ? { ...t, ...updatedTodo } : t));
    }
  }, []);

  const deleteTodo = useCallback(async (id: number): Promise<void> => {
    const success = await todoService.deleteTodo(id);
    if (success) {
      setTodos(prev => prev.filter(t => t.id !== id));
    }
  }, []);

  useEffect(() => {
    loadTodos();
  }, [loadTodos]);

  return {
    todos,
    isLoading,
    loadTodos,
    getTodo,
    createTodo,
    updateTodo,
    deleteTodo,
  };
};

Common Components

// src/components/common/ActivityIndicator.tsx
import React from 'react';
import { ActivityIndicator as ExpoActivityIndicator, StyleSheet, View } from 'react-native';

interface Props {
  isLoading: boolean;
}

export const ActivityIndicator: React.FC<Props> = ({ isLoading }) => {
  if (!isLoading) return null;

  return (
    <View style={styles.container}>
      <ExpoActivityIndicator size="large" color="#007AFF" />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'rgba(255, 255, 255, 0.8)',
    zIndex: 1000,
  },
});

Screen Components

// src/screens/LoginScreen.tsx
import React, { useState } from 'react';
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  StyleSheet,
  Alert,
  KeyboardAvoidingView,
  Platform,
} from 'react-native';
import { useAuth } from '../hooks/useAuth';
import { ActivityIndicator } from '../components/common/ActivityIndicator';

export const LoginScreen: React.FC<{ navigation: any }> = ({ navigation }) => {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const { isLoading, login } = useAuth();

  const handleLogin = async () => {
    if (!username || !password) {
      Alert.alert('Login failed', 'Username and password are required.');
      return;
    }

    const success = await login(username, password);
    if (success) {
      navigation.replace('TodoList');
    } else {
      Alert.alert('Login failed', 'Invalid username or password.');
    }
  };

  return (
    <KeyboardAvoidingView 
      style={styles.container}
      behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
    >
      <ActivityIndicator isLoading={isLoading} />
      
      <View style={styles.content}>
        <Text style={styles.title}>Todo App</Text>
        <Text style={styles.subtitle}>Manage your tasks efficiently</Text>
        
        <TextInput
          style={styles.input}
          placeholder="Username"
          placeholderTextColor="#999"
          value={username}
          onChangeText={setUsername}
          editable={!isLoading}
          autoCapitalize="none"
        />
        
        <TextInput
          style={styles.input}
          placeholder="Password"
          placeholderTextColor="#999"
          value={password}
          onChangeText={setPassword}
          secureTextEntry
          editable={!isLoading}
          autoCapitalize="none"
        />
        
        <TouchableOpacity
          style={[styles.button, isLoading && styles.buttonDisabled]}
          onPress={handleLogin}
          disabled={isLoading}
        >
          <Text style={styles.buttonText}>
            {isLoading ? 'Signing In...' : 'Sign In'}
          </Text>
        </TouchableOpacity>

        <Text style={styles.demoText}>
          Demo credentials:{"\n"}
          username: emilys{"\n"}
          password: emilyspass
        </Text>
      </View>
    </KeyboardAvoidingView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8f9fa',
  },
  content: {
    flex: 1,
    justifyContent: 'center',
    padding: 24,
  },
  title: {
    fontSize: 32,
    fontWeight: 'bold',
    textAlign: 'center',
    marginBottom: 8,
    color: '#1a1a1a',
  },
  subtitle: {
    fontSize: 16,
    textAlign: 'center',
    marginBottom: 48,
    color: '#666',
  },
  input: {
    backgroundColor: 'white',
    padding: 16,
    borderRadius: 12,
    marginBottom: 16,
    borderWidth: 1,
    borderColor: '#e1e5e9',
    fontSize: 16,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.1,
    shadowRadius: 3,
    elevation: 2,
  },
  button: {
    backgroundColor: '#007AFF',
    padding: 16,
    borderRadius: 12,
    alignItems: 'center',
    marginTop: 8,
    shadowColor: '#007AFF',
    shadowOffset: { width: 0, height: 4 },
    shadowOpacity: 0.3,
    shadowRadius: 8,
    elevation: 4,
  },
  buttonDisabled: {
    backgroundColor: '#ccc',
    shadowOpacity: 0,
  },
  buttonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: '600',
  },
  demoText: {
    textAlign: 'center',
    marginTop: 32,
    color: '#666',
    fontSize: 12,
    lineHeight: 18,
  },
});
// src/screens/TodoListScreen.tsx
import React from 'react';
import {
  View,
  Text,
  FlatList,
  TouchableOpacity,
  StyleSheet,
  RefreshControl,
} from 'react-native';
import { useTodos } from '../hooks/useTodos';
import { ActivityIndicator } from '../components/common/ActivityIndicator';
import { TodoItem } from '../models';

export const TodoListScreen: React.FC<{ navigation: any }> = ({ navigation }) => {
  const { todos, isLoading, loadTodos } = useTodos();

  const renderTodoItem = ({ item }: { item: TodoItem }) => (
    <TouchableOpacity
      style={[
        styles.todoItem,
        item.completed && styles.todoItemCompleted
      ]}
      onPress={() => navigation.navigate('TodoDetail', { todo: item })}
    >
      <View style={styles.todoContent}>
        <Text style={[
          styles.todoTitle,
          item.completed && styles.todoTitleCompleted
        ]}>
          {item.title}
        </Text>
        <View style={[
          styles.statusBadge,
          item.completed ? styles.statusCompleted : styles.statusPending
        ]}>
          <Text style={styles.statusText}>
            {item.completed ? 'Completed' : 'Pending'}
          </Text>
        </View>
      </View>
      <View style={styles.todoId}>
        <Text style={styles.todoIdText}>#{item.id}</Text>
      </View>
    </TouchableOpacity>
  );

  return (
    <View style={styles.container}>
      <ActivityIndicator isLoading={isLoading} />
      
      <View style={styles.header}>
        <Text style={styles.headerTitle}>My Todos</Text>
        <Text style={styles.headerSubtitle}>
          {todos.length} tasks total
        </Text>
      </View>
      
      <FlatList
        data={todos}
        renderItem={renderTodoItem}
        keyExtractor={(item) => item.id.toString()}
        style={styles.list}
        refreshControl={
          <RefreshControl
            refreshing={isLoading}
            onRefresh={loadTodos}
            colors={['#007AFF']}
          />
        }
        showsVerticalScrollIndicator={false}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8f9fa',
    padding: 16,
  },
  header: {
    marginBottom: 24,
    paddingTop: 16,
  },
  headerTitle: {
    fontSize: 28,
    fontWeight: 'bold',
    color: '#1a1a1a',
    marginBottom: 4,
  },
  headerSubtitle: {
    fontSize: 16,
    color: '#666',
  },
  list: {
    flex: 1,
  },
  todoItem: {
    backgroundColor: 'white',
    padding: 16,
    borderRadius: 12,
    marginBottom: 12,
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
    borderLeftWidth: 4,
    borderLeftColor: '#007AFF',
  },
  todoItemCompleted: {
    borderLeftColor: '#34C759',
    opacity: 0.8,
  },
  todoContent: {
    flex: 1,
    marginRight: 12,
  },
  todoTitle: {
    fontSize: 16,
    color: '#1a1a1a',
    marginBottom: 8,
    lineHeight: 20,
  },
  todoTitleCompleted: {
    textDecorationLine: 'line-through',
    color: '#666',
  },
  statusBadge: {
    paddingHorizontal: 8,
    paddingVertical: 4,
    borderRadius: 6,
    alignSelf: 'flex-start',
  },
  statusCompleted: {
    backgroundColor: '#34C75920',
  },
  statusPending: {
    backgroundColor: '#FF3B3020',
  },
  statusText: {
    fontSize: 12,
    fontWeight: '600',
  },
  todoId: {
    backgroundColor: '#f1f2f6',
    padding: 8,
    borderRadius: 6,
  },
  todoIdText: {
    fontSize: 12,
    color: '#666',
    fontWeight: '600',
  },
});
// src/screens/TodoDetailScreen.tsx
import React, { useState } from 'react';
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  StyleSheet,
  ScrollView,
  Switch,
  Alert,
} from 'react-native';
import { useTodos } from '../hooks/useTodos';

export const TodoDetailScreen: React.FC<{ navigation: any; route: any }> = ({ 
  navigation, 
  route 
}) => {
  const { todo: initialTodo } = route.params;
  const [todo, setTodo] = useState(initialTodo);
  const { updateTodo, deleteTodo } = useTodos();

  const handleSave = async () => {
    try {
      await updateTodo(todo.id, {
        title: todo.title,
        completed: todo.completed,
      });
      Alert.alert('Success', 'Todo updated successfully!');
    } catch (error) {
      Alert.alert('Error', 'Failed to update todo');
    }
  };

  const handleDelete = async () => {
    Alert.alert(
      'Delete Todo',
      'Are you sure you want to delete this todo?',
      [
        { text: 'Cancel', style: 'cancel' },
        { 
          text: 'Delete', 
          style: 'destructive',
          onPress: async () => {
            await deleteTodo(todo.id);
            navigation.goBack();
          }
        },
      ]
    );
  };

  return (
    <ScrollView style={styles.container}>
      <View style={styles.card}>
        <Text style={styles.label}>Title</Text>
        <TextInput
          style={styles.input}
          value={todo.title}
          onChangeText={(text) => setTodo({ ...todo, title: text })}
          multiline
        />
        
        <View style={styles.switchContainer}>
          <Text style={styles.label}>Status</Text>
          <View style={styles.switchWrapper}>
            <Text style={[
              styles.statusText,
              todo.completed ? styles.statusCompleted : styles.statusPending
            ]}>
              {todo.completed ? 'Completed' : 'Pending'}
            </Text>
            <Switch
              value={todo.completed}
              onValueChange={(value) => setTodo({ ...todo, completed: value })}
              trackColor={{ false: '#f1f2f6', true: '#34C759' }}
              thumbColor={todo.completed ? '#fff' : '#fff'}
            />
          </View>
        </View>

        <View style={styles.infoContainer}>
          <View style={styles.infoItem}>
            <Text style={styles.infoLabel}>Todo ID</Text>
            <Text style={styles.infoValue}>#{todo.id}</Text>
          </View>
          <View style={styles.infoItem}>
            <Text style={styles.infoLabel}>User ID</Text>
            <Text style={styles.infoValue}>#{todo.userId}</Text>
          </View>
        </View>
      </View>

      <TouchableOpacity style={styles.saveButton} onPress={handleSave}>
        <Text style={styles.saveButtonText}>Save Changes</Text>
      </TouchableOpacity>

      <TouchableOpacity style={styles.deleteButton} onPress={handleDelete}>
        <Text style={styles.deleteButtonText}>Delete Todo</Text>
      </TouchableOpacity>

      <TouchableOpacity style={styles.backButton} onPress={() => navigation.goBack()}>
        <Text style={styles.backButtonText}>Back to List</Text>
      </TouchableOpacity>
    </ScrollView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f8f9fa',
    padding: 16,
  },
  card: {
    backgroundColor: 'white',
    padding: 20,
    borderRadius: 12,
    marginBottom: 16,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  label: {
    fontSize: 16,
    fontWeight: '600',
    marginBottom: 8,
    color: '#1a1a1a',
  },
  input: {
    fontSize: 16,
    padding: 12,
    borderWidth: 1,
    borderColor: '#e1e5e9',
    borderRadius: 8,
    marginBottom: 20,
    backgroundColor: '#f8f9fa',
    minHeight: 100,
    textAlignVertical: 'top',
  },
  switchContainer: {
    marginBottom: 20,
  },
  switchWrapper: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
  },
  statusText: {
    fontSize: 16,
    fontWeight: '600',
  },
  statusCompleted: {
    color: '#34C759',
  },
  statusPending: {
    color: '#FF3B30',
  },
  infoContainer: {
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
  infoItem: {
    flex: 1,
  },
  infoLabel: {
    fontSize: 14,
    color: '#666',
    marginBottom: 4,
  },
  infoValue: {
    fontSize: 16,
    fontWeight: '600',
    color: '#1a1a1a',
  },
  saveButton: {
    backgroundColor: '#007AFF',
    padding: 16,
    borderRadius: 12,
    alignItems: 'center',
    marginBottom: 12,
  },
  saveButtonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: '600',
  },
  deleteButton: {
    backgroundColor: '#FF3B30',
    padding: 16,
    borderRadius: 12,
    alignItems: 'center',
    marginBottom: 12,
  },
  deleteButtonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: '600',
  },
  backButton: {
    backgroundColor: '#8e8e93',
    padding: 16,
    borderRadius: 12,
    alignItems: 'center',
    marginBottom: 32,
  },
  backButtonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: '600',
  },
});

Navigation Setup

// src/components/navigation/AppNavigator.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { LoginScreen } from '../../screens/LoginScreen';
import { TodoListScreen } from '../../screens/TodoListScreen';
import { TodoDetailScreen } from '../../screens/TodoDetailScreen';

export type RootStackParamList = {
  Login: undefined;
  TodoList: undefined;
  TodoDetail: { todo: any };
};

const Stack = createNativeStackNavigator<RootStackParamList>();

export const AppNavigator: React.FC = () => {
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Login">
        <Stack.Screen 
          name="Login" 
          component={LoginScreen}
          options={{ headerShown: false }}
        />
        <Stack.Screen 
          name="TodoList" 
          component={TodoListScreen}
          options={{ 
            title: 'My Todos',
            headerBackVisible: false,
          }}
        />
        <Stack.Screen 
          name="TodoDetail" 
          component={TodoDetailScreen}
          options={{ title: 'Todo Details' }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
};

Main App Component (Expo)

// App.tsx
import React from 'react';
import { StatusBar } from 'expo-status-bar';
import { AppNavigator } from './src/components/navigation/AppNavigator';

export default function App() {
  return (
    <>
      <StatusBar style="auto" />
      <AppNavigator />
    </>
  );
}

Key Expo Benefits for .NET Devs:

  1. Simplified Setup: No need to install Android Studio/Xcode for basic development
  2. Expo Go App: Test on physical devices by scanning QR code
  3. Managed Workflow: Expo handles native build configuration
  4. Expo SDK: Pre-built modules for common mobile features (camera, location, etc.)
  5. Over-the-Air Updates: Update your app without app store submissions
  6. TypeScript Native: Expo has excellent TypeScript support out of the box

Demo Credentials:

The app uses dummyjson.com mock API. Use:

  • Username: emilys
  • Password: emilyspass
React Native without a Framework

Create New React Native Project

sudo npx react-native init TodoApp --template react-native-template-typescript
cd TodoApp

Running the App

# Install dependencies
npm install

# For iOS
npx react-native run-ios

# For Android
npx react-native run-android

Project Structure

TodoApp/
├── src/
│   ├── components/
│   │   ├── common/
│   │   │   ├── ActivityIndicator.tsx
│   │   │   └── SafeAreaView.tsx
│   │   ├── login/
│   │   │   └── LoginForm.tsx
│   │   ├── todos/
│   │   │   ├── TodoList.tsx
│   │   │   └── TodoDetail.tsx
│   │   └── navigation/
│   │       └── AppNavigator.tsx
│   ├── models/
│   │   ├── TodoItem.ts
│   │   ├── User.ts
│   │   └── TokenResponse.ts
│   ├── services/
│   │   └── TodoService.ts
│   ├── hooks/
│   │   ├── useAuth.ts
│   │   └── useTodos.ts
│   ├── context/
│   │   └── AuthContext.tsx
│   ├── utils/
│   │   ├── storage.ts
│   │   └── converters.ts
│   └── screens/
│       ├── LoginScreen.tsx
│       ├── TodoListScreen.tsx
│       └── TodoDetailScreen.tsx
├── App.tsx
├── package.json
├── tsconfig.json
└── metro.config.js

File-by-File Implementation

TypeScript Models

// src/models/TokenResponse.ts
export interface TokenResponse {
  token_type?: string;
  expires_in: number;
  ext_expires_in: number;
  access_token?: string;
}
// src/models/TodoItem.ts
export interface TodoItem {
  id: number;
  userId: number;
  title: string;
  completed: boolean;
}
// src/models/User.ts
export interface User {
  id: number;
  username: string;
  password: string;
  email: string;
}

export interface UserResponse {
  users: User[];
}

Storage Utility (Equivalent to Settings.cs)

// src/utils/storage.ts
import AsyncStorage from '@react-native-async-storage/async-storage';

export const Storage = {
  // Username
  getUsername: async (): Promise<string> => {
    return (await AsyncStorage.getItem('username')) || '';
  },
  setUsername: async (username: string): Promise<void> => {
    await AsyncStorage.setItem('username', username);
  },

  // Password
  getPassword: async (): Promise<string> => {
    return (await AsyncStorage.getItem('password')) || '';
  },
  setPassword: async (password: string): Promise<void> => {
    await AsyncStorage.setItem('password', password);
  },

  // Access Token
  getAccessToken: async (): Promise<string> => {
    return (await AsyncStorage.getItem('accessToken')) || '';
  },
  setAccessToken: async (token: string): Promise<void> => {
    await AsyncStorage.setItem('accessToken', token);
  },

  // Token Expiration
  getAccessTokenExpiration: async (): Promise<Date> => {
    const expiration = await AsyncStorage.getItem('accessTokenExpiration');
    return expiration ? new Date(expiration) : new Date();
  },
  setAccessTokenExpiration: async (expiration: Date): Promise<void> => {
    await AsyncStorage.setItem('accessTokenExpiration', expiration.toISOString());
  },
};

Todo Service (HTTP Operations)

// src/services/TodoService.ts
import { TodoItem, UserResponse } from '../models';

export class TodoService {
  private baseUrl = 'https://jsonplaceholder.typicode.com';
  private dummyJsonUrl = 'https://dummyjson.com';

  async login(username: string, password: string): Promise<boolean> {
    try {
      // Using dummyjson.com for mock authentication
      const response = await fetch(`${this.dummyJsonUrl}/users`);
      const userResponse: UserResponse = await response.json();

      if (userResponse.users) {
        const user = userResponse.users.find(
          u => u.username.toLowerCase() === username.toLowerCase() && u.password === password
        );
        return user !== undefined;
      }
    } catch (error) {
      console.error('Login failed:', error);
    }

    return false;
  }

  async getTodos(): Promise<TodoItem[]> {
    try {
      const response = await fetch(`${this.baseUrl}/todos`);
      return await response.json();
    } catch (error) {
      console.error('Failed to fetch todos:', error);
      return [];
    }
  }

  async getTodo(id: number): Promise<TodoItem | null> {
    try {
      const response = await fetch(`${this.baseUrl}/todos/${id}`);
      return await response.json();
    } catch (error) {
      console.error('Failed to fetch todo:', error);
      return null;
    }
  }

  async createTodo(todo: Omit<TodoItem, 'id'>): Promise<TodoItem | null> {
    try {
      const response = await fetch(`${this.baseUrl}/todos`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(todo),
      });
      return await response.json();
    } catch (error) {
      console.error('Failed to create todo:', error);
      return null;
    }
  }

  async updateTodo(id: number, todo: Partial<TodoItem>): Promise<TodoItem | null> {
    try {
      const response = await fetch(`${this.baseUrl}/todos/${id}`, {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(todo),
      });
      return await response.json();
    } catch (error) {
      console.error('Failed to update todo:', error);
      return null;
    }
  }

  async deleteTodo(id: number): Promise<boolean> {
    try {
      const response = await fetch(`${this.baseUrl}/todos/${id}`, {
        method: 'DELETE',
      });
      return response.ok;
    } catch (error) {
      console.error('Failed to delete todo:', error);
      return false;
    }
  }
}

Custom Hooks (React Hooks equivalent to ViewModels)

// src/hooks/useAuth.ts
import { useState, useCallback } from 'react';
import { TodoService } from '../services/TodoService';
import { Storage } from '../utils/storage';

export const useAuth = () => {
  const [isLoading, setIsLoading] = useState(false);
  const todoService = new TodoService();

  const login = useCallback(async (username: string, password: string): Promise<boolean> => {
    if (!username || !password) {
      return false;
    }

    setIsLoading(true);
    try {
      const isAuthenticated = await todoService.login(username, password);
      
      if (isAuthenticated) {
        await Storage.setUsername(username);
        await Storage.setPassword(password);
        // In a real app, you'd store the actual token
        await Storage.setAccessToken('mock-token');
        await Storage.setAccessTokenExpiration(new Date(Date.now() + 3600000)); // 1 hour
      }
      
      return isAuthenticated;
    } catch (error) {
      console.error('Login error:', error);
      return false;
    } finally {
      setIsLoading(false);
    }
  }, []);

  const logout = useCallback(async (): Promise<void> => {
    await AsyncStorage.multiRemove(['username', 'password', 'accessToken', 'accessTokenExpiration']);
  }, []);

  return {
    isLoading,
    login,
    logout,
  };
};
// src/hooks/useTodos.ts
import { useState, useCallback, useEffect } from 'react';
import { TodoItem } from '../models';
import { TodoService } from '../services/TodoService';

export const useTodos = () => {
  const [todos, setTodos] = useState<TodoItem[]>([]);
  const [isLoading, setIsLoading] = useState(false);
  const todoService = new TodoService();

  const loadTodos = useCallback(async () => {
    setIsLoading(true);
    try {
      const fetchedTodos = await todoService.getTodos();
      setTodos(fetchedTodos);
    } catch (error) {
      console.error('Failed to load todos:', error);
    } finally {
      setIsLoading(false);
    }
  }, []);

  const getTodo = useCallback(async (id: number): Promise<TodoItem | null> => {
    return await todoService.getTodo(id);
  }, []);

  const createTodo = useCallback(async (todo: Omit<TodoItem, 'id'>): Promise<void> => {
    const newTodo = await todoService.createTodo(todo);
    if (newTodo) {
      setTodos(prev => [...prev, newTodo]);
    }
  }, []);

  const updateTodo = useCallback(async (id: number, todo: Partial<TodoItem>): Promise<void> => {
    const updatedTodo = await todoService.updateTodo(id, todo);
    if (updatedTodo) {
      setTodos(prev => prev.map(t => t.id === id ? { ...t, ...updatedTodo } : t));
    }
  }, []);

  const deleteTodo = useCallback(async (id: number): Promise<void> => {
    const success = await todoService.deleteTodo(id);
    if (success) {
      setTodos(prev => prev.filter(t => t.id !== id));
    }
  }, []);

  // Load todos on mount
  useEffect(() => {
    loadTodos();
  }, [loadTodos]);

  return {
    todos,
    isLoading,
    loadTodos,
    getTodo,
    createTodo,
    updateTodo,
    deleteTodo,
  };
};

Common Components

// src/components/common/ActivityIndicator.tsx
import React from 'react';
import { ActivityIndicator as RNActivityIndicator, StyleSheet, View } from 'react-native';

interface Props {
  isLoading: boolean;
}

export const ActivityIndicator: React.FC<Props> = ({ isLoading }) => {
  if (!isLoading) return null;

  return (
    <View style={styles.container}>
      <RNActivityIndicator size="large" color="#0000ff" />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'rgba(255, 255, 255, 0.7)',
    zIndex: 1000,
  },
});

Screen Components

// src/screens/LoginScreen.tsx
import React, { useState } from 'react';
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  StyleSheet,
  Alert,
} from 'react-native';
import { useAuth } from '../hooks/useAuth';
import { ActivityIndicator } from '../components/common/ActivityIndicator';

export const LoginScreen: React.FC<{ navigation: any }> = ({ navigation }) => {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const { isLoading, login } = useAuth();

  const handleLogin = async () => {
    if (!username || !password) {
      Alert.alert('Login failed', 'Username and password are required.');
      return;
    }

    const success = await login(username, password);
    if (success) {
      navigation.replace('TodoList');
    } else {
      Alert.alert('Login failed', 'Invalid username or password.');
    }
  };

  return (
    <View style={styles.container}>
      <ActivityIndicator isLoading={isLoading} />
      
      <Text style={styles.title}>Todo App</Text>
      
      <TextInput
        style={styles.input}
        placeholder="Username"
        value={username}
        onChangeText={setUsername}
        editable={!isLoading}
      />
      
      <TextInput
        style={styles.input}
        placeholder="Password"
        value={password}
        onChangeText={setPassword}
        secureTextEntry
        editable={!isLoading}
      />
      
      <TouchableOpacity
        style={[styles.button, isLoading && styles.buttonDisabled]}
        onPress={handleLogin}
        disabled={isLoading}
      >
        <Text style={styles.buttonText}>Login</Text>
      </TouchableOpacity>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    padding: 20,
    backgroundColor: '#f5f5f5',
  },
  title: {
    fontSize: 32,
    fontWeight: 'bold',
    textAlign: 'center',
    marginBottom: 40,
    color: '#333',
  },
  input: {
    backgroundColor: 'white',
    padding: 15,
    borderRadius: 8,
    marginBottom: 15,
    borderWidth: 1,
    borderColor: '#ddd',
  },
  button: {
    backgroundColor: '#007AFF',
    padding: 15,
    borderRadius: 8,
    alignItems: 'center',
  },
  buttonDisabled: {
    backgroundColor: '#ccc',
  },
  buttonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: '600',
  },
});
// src/screens/TodoListScreen.tsx
import React from 'react';
import {
  View,
  Text,
  FlatList,
  TouchableOpacity,
  StyleSheet,
} from 'react-native';
import { useTodos } from '../hooks/useTodos';
import { ActivityIndicator } from '../components/common/ActivityIndicator';
import { TodoItem } from '../models';

export const TodoListScreen: React.FC<{ navigation: any }> = ({ navigation }) => {
  const { todos, isLoading } = useTodos();

  const renderTodoItem = ({ item }: { item: TodoItem }) => (
    <TouchableOpacity
      style={styles.todoItem}
      onPress={() => navigation.navigate('TodoDetail', { todo: item })}
    >
      <Text style={styles.todoTitle}>{item.title}</Text>
      <Text style={[
        styles.todoStatus,
        item.completed ? styles.completed : styles.pending
      ]}>
        {item.completed ? 'Completed' : 'Pending'}
      </Text>
    </TouchableOpacity>
  );

  return (
    <View style={styles.container}>
      <ActivityIndicator isLoading={isLoading} />
      
      <Text style={styles.header}>My Todos</Text>
      
      <FlatList
        data={todos}
        renderItem={renderTodoItem}
        keyExtractor={(item) => item.id.toString()}
        style={styles.list}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    backgroundColor: '#f5f5f5',
  },
  header: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 20,
    color: '#333',
  },
  list: {
    flex: 1,
  },
  todoItem: {
    backgroundColor: 'white',
    padding: 15,
    borderRadius: 8,
    marginBottom: 10,
    borderLeftWidth: 4,
    borderLeftColor: '#007AFF',
  },
  todoTitle: {
    fontSize: 16,
    marginBottom: 5,
    color: '#333',
  },
  todoStatus: {
    fontSize: 14,
    fontWeight: '500',
  },
  completed: {
    color: '#34C759',
  },
  pending: {
    color: '#FF3B30',
  },
});
// src/screens/TodoDetailScreen.tsx
import React from 'react';
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  StyleSheet,
} from 'react-native';

export const TodoDetailScreen: React.FC<{ navigation: any; route: any }> = ({ 
  navigation, 
  route 
}) => {
  const { todo } = route.params;

  return (
    <View style={styles.container}>
      <Text style={styles.header}>Todo Details</Text>
      
      <View style={styles.detailCard}>
        <Text style={styles.label}>Title:</Text>
        <TextInput
          style={styles.input}
          value={todo.title}
          onChangeText={() => {}} // In a real app, you'd update the state
        />
        
        <Text style={styles.label}>Status:</Text>
        <Text style={[
          styles.status,
          todo.completed ? styles.completed : styles.pending
        ]}>
          {todo.completed ? 'Completed' : 'Pending'}
        </Text>
        
        <Text style={styles.label}>User ID:</Text>
        <Text style={styles.value}>{todo.userId}</Text>
      </View>
      
      <TouchableOpacity
        style={styles.backButton}
        onPress={() => navigation.goBack()}
      >
        <Text style={styles.backButtonText}>Back to List</Text>
      </TouchableOpacity>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    backgroundColor: '#f5f5f5',
  },
  header: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 20,
    color: '#333',
  },
  detailCard: {
    backgroundColor: 'white',
    padding: 20,
    borderRadius: 8,
    marginBottom: 20,
  },
  label: {
    fontSize: 16,
    fontWeight: '600',
    marginBottom: 5,
    color: '#666',
  },
  value: {
    fontSize: 16,
    marginBottom: 15,
    color: '#333',
  },
  input: {
    fontSize: 16,
    padding: 10,
    borderWidth: 1,
    borderColor: '#ddd',
    borderRadius: 4,
    marginBottom: 15,
    backgroundColor: '#f9f9f9',
  },
  status: {
    fontSize: 16,
    fontWeight: '500',
    marginBottom: 15,
  },
  completed: {
    color: '#34C759',
  },
  pending: {
    color: '#FF3B30',
  },
  backButton: {
    backgroundColor: '#007AFF',
    padding: 15,
    borderRadius: 8,
    alignItems: 'center',
  },
  backButtonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: '600',
  },
});

Navigation Setup

// src/components/navigation/AppNavigator.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { LoginScreen } from '../../screens/LoginScreen';
import { TodoListScreen } from '../../screens/TodoListScreen';
import { TodoDetailScreen } from '../../screens/TodoDetailScreen';

export type RootStackParamList = {
  Login: undefined;
  TodoList: undefined;
  TodoDetail: { todo: any };
};

const Stack = createNativeStackNavigator<RootStackParamList>();

export const AppNavigator: React.FC = () => {
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Login">
        <Stack.Screen 
          name="Login" 
          component={LoginScreen}
          options={{ headerShown: false }}
        />
        <Stack.Screen 
          name="TodoList" 
          component={TodoListScreen}
          options={{ title: 'Todo List' }}
        />
        <Stack.Screen 
          name="TodoDetail" 
          component={TodoDetailScreen}
          options={{ title: 'Todo Details' }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
};

Main App Component

// App.tsx
import React from 'react';
import { StatusBar } from 'react-native';
import { AppNavigator } from './src/components/navigation/AppNavigator';

const App: React.FC = () => {
  return (
    <>
      <StatusBar barStyle="dark-content" />
      <AppNavigator />
    </>
  );
};

export default App;

Package Dependencies

Update your package.json:

{
  "dependencies": {
    "react": "18.2.0",
    "react-native": "0.72.0",
    "@react-navigation/native": "^6.1.0",
    "@react-navigation/native-stack": "^6.9.0",
    "@react-native-async-storage/async-storage": "^1.18.0",
    "react-native-safe-area-context": "^4.6.0",
    "react-native-screens": "^3.22.0"
  },
  "devDependencies": {
    "@types/react": "^18.0.0",
    "@types/react-native": "^0.72.0",
    "typescript": "^5.0.0"
  }
}

Key React Native Concepts Explained

Components vs Pages

  • In React Native, we use functional components instead of XAML pages
  • Each screen is a component that returns JSX

Hooks vs ViewModels

  • useState = replaces property change notifications
  • useEffect = replaces OnAppearing lifecycle methods
  • Custom hooks = replace ViewModels

Navigation

  • React Navigation replaces XAML Shell navigation
  • Stack navigator manages screen transitions

State Management

  • Local state with useState for component-specific data
  • Custom hooks for shared business logic

Styling

  • StyleSheet API instead of XAML styling
  • Flexbox-based layout system

Data Storage

  • AsyncStorage replaces Xamarin Essentials Preferences
  • Async/await for asynchronous operations

React TypeScript Folder Structure For D365

Creating a folder structure for a React TypeScript web application inspired by the Dynamics 365 (D365) Application Object Tree (AOT) can help you organize your code effectively. Here's a suggested folder structure:
react-d365-app/
│
├── public/
│   ├── index.html
│   ├── favicon.ico
│   └── assets/               # Static assets like images, fonts, etc.
│
├── src/
│   ├── components/           # Reusable components
│   │   ├── Button/           # Example reusable component
│   │   │   ├── Button.tsx
│   │   │   ├── Button.css
│   │   │   └── index.ts
│   │   ├── Modal/
│   │   │   ├── Modal.tsx
│   │   │   ├── Modal.css
│   │   │   └── index.ts
│   │   └── ...               # Other reusable components
│   │
│   ├── classes/              # Classes from D365 AOT
│   │   ├── ClassName1.ts
│   │   ├── ClassName2.ts
│   │   └── ...               # Other classes
│   │
│   ├── forms/                # Forms from D365 AOT
│   │   ├── FormName1/
│   │   │   ├── FormName1.tsx
│   │   │   ├── FormName1.css
│   │   │   └── index.ts
│   │   ├── FormName2/
│   │   │   ├── FormName2.tsx
│   │   │   ├── FormName2.css
│   │   │   └── index.ts
│   │   └── ...               # Other forms
│   │
│   ├── menus/                # Menus from D365 AOT
│   │   ├── MenuName1/
│   │   │   ├── MenuName1.tsx
│   │   │   └── index.ts
│   │   ├── MenuName2/
│   │   │   ├── MenuName2.tsx
│   │   │   └── index.ts
│   │   └── ...               # Other menus
│   │
│   ├── services/             # API services and business logic
│   │   ├── apiService.ts
│   │   ├── dataService.ts
│   │   └── ...               # Other services
│   │
│   ├── hooks/                # Custom React hooks
│   │   ├── useCustomHook.ts
│   │   └── ...
│   │
│   ├── contexts/             # Context providers
│   │   ├── AuthContext.tsx
│   │   └── ...
│   │
│   ├── utils/                # Utility functions
│   │   ├── helpers.ts
│   │   └── constants.ts
│   │
│   ├── App.tsx               # Main app component
│   ├── index.tsx             # Entry point
│   └── styles/               # Global styles
│       ├── index.css
│       └── ...
│
├── .gitignore
├── package.json
├── tsconfig.json
└── README.md
To organize a React TypeScript web app in an MVC (Model-View-Controller) pattern inspired by the Dynamics 365 AOT, you can structure your folders to clearly separate the concerns of Models, Views, and Controllers. Here’s a suggested folder structure:
react-mvc-d365-app/
│
├── public/
│   ├── index.html
│   ├── favicon.ico
│   └── assets/               # Static assets like images, fonts, etc.
│
├── src/
│   ├── models/               # Data models
│   │   ├── UserModel.ts
│   │   ├── ProductModel.ts
│   │   └── ...               # Other models
│   │
│   ├── views/                # UI components (Views)
│   │   ├── HomeView/
│   │   │   ├── HomeView.tsx
│   │   │   ├── HomeView.css
│   │   │   └── index.ts
│   │   ├── UserView/
│   │   │   ├── UserView.tsx
│   │   │   ├── UserView.css
│   │   │   └── index.ts
│   │   └── ...               # Other views
│   │
│   ├── controllers/          # Controllers handling logic
│   │   ├── UserController.ts
│   │   ├── ProductController.ts
│   │   └── ...               # Other controllers
│   │
│   ├── components/           # Reusable components
│   │   ├── Button/
│   │   │   ├── Button.tsx
│   │   │   ├── Button.css
│   │   │   └── index.ts
│   │   ├── Modal/
│   │   │   ├── Modal.tsx
│   │   │   ├── Modal.css
│   │   │   └── index.ts
│   │   └── ...               # Other reusable components
│   │
│   ├── services/             # API services and business logic
│   │   ├── apiService.ts
│   │   ├── dataService.ts
│   │   └── ...               # Other services
│   │
│   ├── hooks/                # Custom React hooks
│   │   ├── useCustomHook.ts
│   │   └── ...
│   │
│   ├── contexts/             # Context providers
│   │   ├── AuthContext.tsx
│   │   └── ...
│   │
│   ├── utils/                # Utility functions
│   │   ├── helpers.ts
│   │   └── constants.ts
│   │
│   ├── App.tsx               # Main app component
│   ├── index.tsx             # Entry point
│   └── styles/               # Global styles
│       ├── index.css
│       └── ...
│
├── .gitignore
├── package.json
├── tsconfig.json
└── README.md

React TypeScript App with Material UI

Create React TypeScript App with Material UI
npx create-react-app@latest cyberswissblade --template typescript@latest
npm install @mui/material @emotion/react @emotion/styled
npm install @fontsource/roboto
npm install @mui/icons-material

https://codeload.github.com/mui/material-ui/tar.gz/master | tar -xz --strip=2 material-ui-master/examples/material-ui-cra-ts

  • Deprecation warnings
    • node:24536) [DEP_WEBPACK_DEV_SERVER_ON_AFTER_SETUP_MIDDLEWARE] DeprecationWarning: 'onAfterSetupMiddleware' option is deprecated. Please use the 'setupMiddlewares' option.
    • (Use node --trace-deprecation ... to show where the warning was created)
    • (node:24536) [DEP_WEBPACK_DEV_SERVER_ON_BEFORE_SETUP_MIDDLEWARE] DeprecationWarning: 'onBeforeSetupMiddleware' option is deprecated. Please use the 'setupMiddlewares' option.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment