ReactJS
ReactNative
npx create-expo-app demo --template blank
cd demo
# Version Web
npx expo install react-dom react-native-web @expo/metro-runtime
# Live reload en Web
sed -i "1 i import '@expo/metro-runtime';" index.js
# Utilitaires multi-plateformes
npx expo install react-native-safe-area-context toastify-react-native react-native-vector-icons react-native-simple-dialogs
npx expo start --tunnel
├── App.js # Point d'entrée du code, package.json > main: ...
├── app.json # Metadonnées du projet: nom, version, configurations...
├── assets # Ressources statiques: images, sons, pdf, etc.
│ ├── icon.png
│ └── splash.png
├── babel.config.js # Configuration de build
├── .expo # Fichiers locaux de l'environnement de build Expo
├── .gitignore
├── package.json # Liste des dépendances JS
└── package-lock.json
import { useState } from 'react';
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, View, TouchableHighlight, ScrollView, Switch, Button, Text, TextInput } from 'react-native';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import ToastManager, { Toast } from 'toastify-react-native'
import { Dialog } from 'react-native-simple-dialogs';
import { Ionicons } from '@expo/vector-icons';
function TodoItem({ todo, isLast, setToggling }) {
return (
<TouchableHighlight onPress={ () => setToggling(todo) }>
<View
style={{
flexDirection: 'row',
gap: 8,
paddingVertical: 16,
paddingHorizontal: 8,
backgroundColor: 'white', /* requis pour highlight */
borderBottomWidth: isLast ? 0 : 1,
borderBottomColor: 'lightgray'
}}
>
<Ionicons name="checkmark-circle" size={22} color={ todo.done ? 'green' : 'lightgray' } />
<View style={{ flex: 1, flexDirection: 'column' }}>
<Text style={[styles.text, { fontWeight: 'bold' }]}>{ todo.name }</Text>
{
!!todo.description &&
<Text style={ styles.text }>{ todo.description }</Text>
}
</View>
</View>
</TouchableHighlight>
)
}
export default function App() {
const EMPTY_TODO = () => { return {
name: null,
done: false,
description: null
}}
const SEED_COUNT = 4;
const SEED = [...Array(SEED_COUNT).keys()].map((item, index, array) => {
const name = `Todo ${item}`
return {
id: Math.random().toString(16).substring(2),
name: name,
description: `Description ${name} `.repeat(index),
done: (index % 3)
}
})
const [todos, setTodos] = useState(SEED);
const [newTodo, setNewTodo] = useState(EMPTY_TODO());
const [toggling, setToggling] = useState(null)
function add() {
if ((newTodo.name?.trim() ?? '') == '') {
Toast.error('Provide a Todo name', 'bottom')
} else {
newTodo.id = Math.random().toString(16).substring(2);
newTodo.name = newTodo.name.trim()
setTodos([newTodo, ...todos]);
setNewTodo(EMPTY_TODO());
}
}
function toggle(id) {
setTodos(
todos.map( t => {
if (t.id == id) {
return {
...t,
done : !t.done
}
}
return t;
})
)
}
function list() {
if (todos.length == 0) {
return (
<View style={{ flexGrow: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text style={{ fontSize: 48, color: 'gray' }}>No todos...</Text>
</View>
)
} else {
/* Pas la facon ideale pour afficher une liste, simplification pour la demonstration */
return (
<ScrollView style={{ flexGrow: 1 }}>
{
todos.map((todo, index, array) => {
return (
<TodoItem
key={todo.id}
todo={ todo }
isLast={ index + 1 == array.length }
setToggling={ setToggling }
/>
)
})
}
</ScrollView>
)
}
}
return (
<>
<StatusBar style="auto" />
<ToastManager />
<SafeAreaProvider>
<SafeAreaView style={styles.container}>
<Text style={{ fontSize: 28, fontWeight: 'bold', textAlign: 'center' }}>Todoer Starter</Text>
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
<TextInput
style={[styles.input, { height: 48, flexGrow: 1 }]}
placeholder='New task'
value={ newTodo.name ?? ''}
onChangeText={ (text) => setNewTodo({...newTodo, name: text}) }
/>
<Switch
value={ newTodo.done }
onValueChange={ checked => setNewTodo({...newTodo, done: checked}) }
/>
</View>
<TextInput
style={[styles.input, { width: '100%', height: 96, verticalAlign: 'top' }]}
placeholder='Optional description'
multiline={ true }
/>
<View style={{ flexDirection: 'row', justifyContent: 'center' }}>
{/* Envelopper dans une View pour eviter une largeur de 100% */}
<Button
title='Add'
color='green'
onPress={ add }
/>
</View>
{ list() }
<Dialog
title={ toggling?.name }
visible={ !!toggling }
onTouchOutside={ () => setToggling(null) }
animationType="fade"
>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', gap: 32 }}>
<Button title="Cancel" color="gray" onPress={ () => setToggling(null) } />
<Button title={ toggling?.done ? 'Incomplete' : 'Completed' } onPress={ () => { setToggling(false); toggle(toggling?.id); } } />
</View>
</Dialog>
</SafeAreaView>
</SafeAreaProvider>
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
gap: 16,
justifyContent: 'center',
padding: 16,
},
input: {
borderWidth: 1,
borderColor: 'lightgray',
padding: 8,
},
text: {
fontSize: 18,
},
});
Pour utiliser le debugger via l'environnement distant de la machine virtuelle, il faut récupérer l'URL des Devtools manuellement
sed -i '48 i console.log(`\n\nDebug URL\n${url.origin}/debugger-frontend/rn_fusebox.html?ws=%2Finspector%2Fdebug%3Fdevice%3D${_app_reactNative.logicalDeviceId}%26page%3D3&sources.hide_add_folder=true&unstable_enableNetworkPanel=true\n\n`);' node_modules/@expo/cli/build/src/start/server/middleware/inspector/JsInspector.js
Puis,
Le debugger fonctionne/fonctionnait? uniquement avec un navigateur basé sur Chrome, avec les configurations suivantes: