In more complex applications or components it makes sense to manage the state (model) separately from the view.
Often the entire application state is represented by a data model and every change to the state will be done by triggering a change to the data model.
In the reducer hook, Redux, ngrx and vuex, each state change is triggered by an action, which is represented as a JavaScript object
note: vuex uses the term mutation
In Redux / reducer hook:
example actions:
{
"type": "addTodo",
"payload": "learn React"
}
{
"type": "deleteTodo",
"payload": 1
}
{
"type": "deleteCompletedTodos"
}
Technique that is used in Redux and in React's reducer hook:
Manual usage of a reducer:
const state1 = [
{ id: 1, title: 'groceries', completed: false },
{ id: 2, title: 'taxes', completed: true },
];
const actionA = { type: 'addTodo', payload: 'gardening' };
const state2 = todosReducer(state1, actionA);
const actionB = { type: 'deleteTodo', payload: 1 };
const state3 = todosReducer(state2, actionB);
console.log(state3);
/* [{ id: 2, title: 'taxes', completed: true },
{ id: 3, title: 'gardening', completed: false },] */
reducer implementation:
const todosReducer = (oldState, action) => {
switch (action.type) {
case 'addTodo':
return [
...oldState,
{
title: action.payload,
completed: false,
id: Math.max(0, ...oldState.map((t) => t.id)) + 1,
},
];
case 'deleteTodo':
return oldState.filter(
(todo) => todo.id !== action.payload
);
default:
throw new Error('unknown action type');
}
};
usage with TypeScript:
type TodosState = Array<Todo>;
type TodosAction =
| { type: 'addTodo'; payload: string }
| { type: 'deleteTodo'; payload: number };
const todosReducer = (
state: TodosState,
action: TodosAction
): TodosState => {
// ...
};
reducers can be easily combined / split to manage complex / nested state
state example:
{
"todoData": {
"status": "loading",
"todos": []
},
"uiData": {
"newTitle": "re",
"filterText": ""
}
}
reducer implementation:
const rootReducer = (rootState, action) => ({
todoData: todoDataReducer(rootState.todoData, action),
uiData: uiDataReducer(rootState.uiData, action),
});
const uiDataReducer = (uiData, action) => ({
newTitle: newTitleReducer(uiData.newTitle, action),
filterText: filterTextReducer(uiData.filterText, action),
});
const newTitleReducer = (newTitle, action) => {
if (action.type === 'setNewTitle') {
return action.payload;
} else if (action.type === 'addTodo') {
return '';
} else {
return newTitle;
}
};
When combining reducers, a single reducer only manages part of the state; but every reducer receives any action and may react to it
For managing state we can now also utilize useReducer
in addition to useState
:
const [state, dispatch] = useReducer(reducer, initialState);
specific example:
const [count, countDispatch] = useReducer(countReducer, 0);
Calling useReducer
returns an array with two entries:
const TodoApp = () => {
const [todos, dispatch] = useReducer(
todosReducer,
initialTodos
);
return (
<div>
...
<button
onClick={() => dispatch({ type: 'deleteAll' })}
>
delete all todos
</button>
</div>
);
};
The powerful Redux devtools may be used with the reducer hook: https://github.com/troch/reinspect (requires some configuration work, manually dispatching actions does not work)
working directly with immutable state can be complicated / tedious
helper libraries:
import produce from 'immer';
const todos = [
// ...
];
const newTodos = produce(todos, (todosDraft) => {
todosDraft[0].completed = true;
todosDraft.push({ title: 'study', completed: false });
});
advantages over "plain" frontend code:
required packages:
graphql
graphql-tag
apollo-client
apollo-cache-inmemory
apollo-link-http
react-apollo
(for use with React)import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import gql from 'graphql-tag';
const client = new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: 'https://api.spacex.land/graphql/',
}),
});
// via a tagged template string
const LAUNCHES_QUERY = gql`
query recentLaunches {
launchesPast(limit: 10) {
mission_name
}
}
`;
client
.query({ query: LAUNCHES_QUERY })
.then((result) => console.log(result));
Apollo can also manage local data / local state
Setting local state:
client.writeData
for simple cases@client
directive in GraphQL mutations; and local resolversQuerying local state:
@client
directive in GraphQL queriesSimple version: setting local state directly (similar to React's setState
):
const client = useApolloClient();
client.writeData({ data: { inputText: '' } });
local resolvers for mutations:
https://www.apollographql.com/docs/react/data/local-state/#local-resolvers
Querying local state (via @client
):
const INPUT_TEXT_QUERY = gql`
query {
inputText @client
}
`;
client
.query({ query: INPUT_TEXT_QUERY })
.then((result) => console.log(result));
extension for Chrome
unreliable according to reviews (3.2 / 5 stars)
functionality:
An application usually communicates with only one GraphQL API
import { ApolloProvider } from 'react-apollo';
<ApolloProvider client={client}>
<App />
</ApolloProvider>
const LAUNCHES_QUERY = gql`
query recentLaunches {
launchesPast(limit: 10) {
mission_name
}
}
`;
function RecentLaunches() {
const { data, loading, error } = useQuery(LAUNCHES_QUERY);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error!</div>;
return (
<div>
<h1>Launches</h1>
{data.launchesPast.map((launch) => (
<div>{launch.mission_name}</div>
))}
</div>
);
}
const LAUNCHES_QUERY = gql`
query recentLaunches($numLaunches: Int!) {
launchesPast(limit: $numLaunches) {
mission_name
}
}
`;
function RecentLaunches({ numLaunches }) {
const { data, loading, error } = useQuery(
LAUNCHES_QUERY,
{ variables: { numLaunches } }
);
...
}
Updating every 5 seconds:
const { data, loading, error } = useQuery(LAUNCHES_QUERY, {
pollInterval: 5000,
});
Updating via refetch()
:
const { data, loading, error, refetch } = useQuery(
LAUNCHES_QUERY
);
...
refetch()
Example for todos:
const SET_COMPLETED = gql`
mutation setCompleted($id: ID!, $completed: Boolean!) {
updateTodo(id: $id, input: { completed: $completed }) {
id
completed
}
}
`;
basic usage:
const [setCompleted] = useMutation(SET_COMPLETED);
extended version (cf. useState
):
const [
setCompleted,
{ data, loading, error },
] = useMutation(SET_COMPLETED);
state is changed first on the server and then locally
update of the local cache:
accessing the cache and the reply inside the update
function:
const [addTodo] = useMutation(ADD_TODO, {
update: (cache, reply) => {
// cache: local cache
// reply: reply from the API
console.log(cache);
console.log(reply);
// TODO: update the local cache based on the reply
},
});
const [addTodo] = useMutation(ADD_TODO, {
update: (cache, reply) => {
// get old todos from cache
const oldTodos = cache.readQuery({ query: GET_TODOS })
.todos;
// build newTodos array based on the server response
const newTodos = [...oldTodos, reply.data.createTodo];
// TODO: update the local cache with the newTodos array
},
});
const [addTodo] = useMutation(ADD_TODO, {
update: (cache, reply) => {
const oldTodos = cache.readQuery({ query: GET_TODOS })
.todos;
const newTodos = [...oldTodos, reply.data.createTodo];
cache.writeQuery({
query: GET_TODOS,
data: { todos: newTodos },
});
},
});
an identity provider can verify the identity of users (can authenticate users)
examples:
the current user is logged in as user "foo" on this domain
the current user is authenticated as user "x" by Google / as user "y" by facebook
mechanism for the user:
user clicks on login, is redirected to a login page, and then sent back to the original site once logged in
in the background the user will receive an identity token, a piece of data that can prove their identity with the identity provider
standards:
Auth0 (auth-zero) is a widely-used identity provider
supports authentication via "internal" accounts or external identity providers (e.g. Google, Apple, Facebook, ...)
Application Settings:
for local development, set all three to http://localhost:3000
under Settings:
each Auth0 registrant has at least one domain (e.g. dev-xxxxxxxx.eu.auth0.com)
each app has a specific client ID (e.g. jA0EAwMCxamDRMfOGV5gyZPnyX1BBPOQ)
library: auth0-react
npm package: @auth0/auth0-react
include a provider that manages a context:
<Auth0Provider
domain="YOUR_AUTH0_DOMAIN"
clientId="YOUR_AUTH0_CLIENT_ID"
redirectUri={window.location.origin}
>
<App />
</Auth0Provider>
(see next slide for implementation in next.js)
when using next.js:
// pages/_app.js
export default function App({ Component, pageProps }) {
return (
<Auth0Provider
domain="YOUR_AUTH0_DOMAIN"
clientId="YOUR_AUTH0_CLIENT_ID"
redirectUri="YOUR_URL"
>
<Component {...pageProps} />
</Auth0Provider>
);
}
const auth = useAuth0();
if (auth.isLoading) {
return <div>...</div>;
} else if (auth.error) {
return <div>...</div>;
} else if (!auth.isAuthenticated) {
return (
<button onClick={auth.loginWithRedirect}>Log in</button>
);
} else {
return (
<div>
main content
<button onClick={auth.logout}>log out</button>
</div>
);
}
entries in the return value of useAuth0
:
isLoading
error
isAuthenticated
loginWithRedirect
logout
user
(user.sub
, user.email
, user.username
, user.name
, ...)task: create a React app where the content is only accessible if the user is logged in and the username / email are displayed if available
authentication can be verified via access tokens (these are not JWT tokens)
more entries in the return value of useAuth0
:
getAccessTokenSilently
getAccessTokenWithPopup
making a request with the access token:
async function makeRequestSilently() {
const token = await auth.getAccessTokenSilently();
console.log(`make API request with token ${token}`);
}
verifying an Auth0 token on the API side:
const auth0Domain = 'dev-xxxxxxxx.eu.auth0.com';
try {
const res = await fetch(
`https://${auth0Domain}/userinfo`,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
}
);
const userInfo = await res.json();
console.log(`authenticated as ${userInfo.sub}`);
} catch {
console.log('error');
}
note: just sending user info (e.g. user ID) from the client is not secure since it can't be verified by the API; information can only be verified via a token
Progressive Web Apps with React
Progressive Web Apps enable us to write applications for PC and mobile using HTML, CSS and JavaScript
create-react-app can be used to initialize React projects with basic PWA support
npx create-react-app myapp --template cra-template-pwa
npx create-react-app myapp --template cra-template-pwa-typescript
Codesandbox has built-in support for very basic PWAs
PWA basics in create-react-app projects:
public/manifest.json
src/serviceWorker.js
in index.js
/ index.tsx
:
serviceWorker.register();
Via public/manifest.json
Procedure in Chrome:
see also: https://developers.google.com/web/fundamentals/app-install-banners/
TypeScript implementation:
const [canInstall, setCanInstall] = useState(false);
const installPromptEventRef = useRef<Event>();
const getInstallPermission = () => {
window.addEventListener(
'beforeinstallprompt',
(ipEvent) => {
ipEvent.preventDefault();
installPromptEventRef.current = ipEvent;
setCanInstall(true);
}
);
};
useEffect(getInstallPermission, []);
TypeScript implementation:
<button
disabled={!canInstall}
onClick={() => {
(installPromptEventRef.current as any).prompt();
}}
>
install
</button>
npm run build
React Native can be used to write React applications for iOS and Android devices
options:
installation:
npm install -g expo-cli
creating a new expo project:
expo init myproject
running a project (will open a dashboard at localhost:19002):
npm run start
running on a device:
Examples:
<Button title="press me" onPress={handlePress} />
<TextInput value={myText} onChangeText={setMyText} />
All React Native components accept a style prop that can take an object:
const TodoItem = ({ title, completed }) => (
<View style={{ margin: 5, padding: 8 }}>
<Text>{title}</Text>
</View>
);
The style prop can also receive an array of objects which are merged (falsy entries are ignored)
const TodoItem = ({ title, completed }) => (
<View
style={[
{ padding: 8, backgroundColor: 'lightcoral' },
completed && { backgroundColor: 'lightgrey' },
]}
>
<Text>{title}</Text>
</View>
);
Creating stylesheets that define multiple sets of styles:
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
todoItem: {
padding: 8,
backgroundColor: 'lightcoral',
},
completedView: {
backgroundColor: 'lightgrey',
},
completedText: {
textDecoration: 'line-through',
},
});
using stylesheets:
const TodoItem = ({ title, completed, onToggle }) => (
<View
style={[
styles.todoItem,
completed && styles.completedView,
]}
>
<Text style={[completed && styles.completedText]}>
{completed ? 'DONE: ' : 'TODO: '}
{title}
</Text>
</View>
);
option 1 (simple cases):
import { Platform } from 'react-native';
if (Platform.OS === 'web') {
// 'web' / 'ios' / 'android'
}
option 2 (platform-specific components):
AddTodo.web.js
AddTodo.ios.js
AddTodo.android.js
Example: a Button
component which renders a button
element with additional styling
The Button
component should have the same properties as a button
element
<Button type="submit" disabled={true}>
foo
</Button>
should render:
<button type="submit" disabled={true} className="Button">
foo
</button>
implementation:
function Button(props: ComponentProps<'button'>) {
// return a "button" element with one extra CSS class
return <button {...props} className="Button" />;
}
example: component with an extra property
type Props = ComponentProps<'input'> & {
label: string;
};
function InputWithLabel({ label, ...rest }: Props) {
return (
<label>
{label}: <input {...rest} />
</label>
);
}
Extra wish: The ref property of the Button
component should also point to the button
element
const Button = forwardRef<
HTMLButtonElement,
ComponentProps<'button'>
>((props, ref) => {
return <button {...props} ref={ref} className="Button" />;
});
confusing terminology:
Higher-order components are not components 😲
Higher-order components are functions that modify / enhance a component definition (they are "component decorators")
Example:
React's memo
is a higher-order component
It receives a component and returns a memoized component:
const MemoizedRating = memo(Rating);
Example:
connect
from react-redux returns a HOC:
// connector is a HOC
const connector = connect(
mapStateToProps,
mapDispatchToProps
);
The resulting HOC receives a regular component and returns a component that is connected to the Redux store:
const RatingContainer = connector(Rating);
render props: pattern that allows a parent to provide additional or derived data when rendering a child
the parent receives instructions for how to render data - these instructions are provided as functions
Wishful thinking - if only this would work:
<DataLoader resource="https://jsonplaceholder.typicode.com/todos">
<DataViewer />
</DataLoader>
one way to make it work:
<DataLoader
resource="https://jsonplaceholder.typicode.com/todos"
render={(data) => <DataViewer data={data} />}
/>
full example: DataLoader
<DataLoader
resource="https://jsonplaceholder.typicode.com/todos"
renderLoading={() => <div>loading</div>}
renderError={(error) => <div>Error: ...</div>}
render={(data) => <DataViewer data={data} />}
/>
example: DataTable
<DataTable
data={todos}
filter={(todo) => !todo.completed}
renderRow={(todo) => <tr>{todo.title}</tr>}
/>
example: formik library
<Formik
initialValues={/*...*/}
onSubmit={/*...*/}
validate={/*...*/}
children={(props) => <Form>...</Form>}
/>
libraries:
// src/i18n.ts
import i18next from 'i18next';
import { initReactI18next } from 'react-i18next';
setup with inline data:
// src/i18n.ts
i18next.use(initReactI18next).init({
debug: true,
lng: 'en',
resources: {
en: {
translation: { hello: 'Hello!', sign_in: 'Sign in' },
},
de: {
translation: { hello: 'Hallo!', sign_in: 'Anmelden' },
},
},
});
including in the project in src/index.tsx:
// ...
import './i18n';
// ...
testing its functionality:
import i18next from 'i18next';
console.log(i18next.t('hello'));
console.log(i18next.t('sign_in', { lng: 'de' }));
accessing the translation function via a hook:
function SignInButton() {
const { t } = useTranslation();
return <button>{t('sign_in')}</button>;
}
resources:
Portals: allow rendering of HTML elements "outside" the component they are created in
example: Dialog
component
A Dialog
can be rendered from any component - its rendering will become a child of the HTML body element
function Dialog(props: { children: React.ReactNode }) {
return ReactDOM.createPortal(
<div style={dialogStyle}>{props.children}</div>,
document.querySelector('body') as HTMLBodyElement
);
}
goal:
catch errors in a deployed application and show "nice" error messages to users
example: catch runtime errors in the entire application
<MyErrorBoundary>
<App />
</MyErrorBoundary>
Error boundary components can only be implemented as class components
Error boundaries will catch these errors of subcomponents:
implementation of an ErrorBoundary
component:
type Props = { children: React.ReactNode };
type State = { hasError: boolean };
class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <div>Something went wrong ...</div>;
}
return this.props.children;
}
}