Common Patterns

This guide provides examples of some common patterns developers use to build applications.

Routing#

react-router-dom can be used to handle dynamic routing within your application.

Follow the Routing standards when creating routes.

App.tsx
import { HashRouter as Router, Route, Switch } from "react-router-dom";
// Import your page components to register them as routes
import { List, View } from "./pages";
export const App: React.FC = () => {
return (
<Router>
<Switch>
<Route exact path="/" component={List} />
<Route exact path="/:id" component={View} />
// All other routes are not found (404s)
<Route component={() => <Result status="404" />} />
</Switch>
</Router>
);
};

useParams can be used to access information about the currently viewed route, such as the ID of the resource being accessed.

// For example, when the route is registered as "/resource/:id"
export const View: React.FC = () => {
// If the current location is "/resource/1337" then id = "1337"
const { id } = useParams<{ id: string }>();
return <p>Your are viewing resource {id}</p>;
}

Checking permissions and feature toggles#

Refer to the API documentation for @iqmetrix/authorization for examples of checking permissions and feature toggles within your application.

Displaying a table of resources#

Use @iqmetrix/authenticated-fetch to fetch resources with the user's current authentication token and @iqmetrix/antd as a source of common components.

Note: This is a simple example. As your application increases in complexity, you will want to use more fitting patterns.

import React, { useEffect, useState } from "react";
// Function packages
import authenticatedFetch from "@iqmetrix/authenticated-fetch";
// Component packages
import { PageLayout } from "@iqmetrix/layout";
import { Alert, PageHeader, Table } from "@iqmetrix/antd";
// Define the structure of your data model.
// You will want to define this within its own file to share across your application code.
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
// Define the URL you would like to fetch from.
// You will want to define this within its own file to share across your application.
// You can use a function when you need to calculate pieces of the URL.
// getEnvironmentSuffix() is commonly used to interact with the proper environment's service
// E.g. cosnt URL = async () => `https://service${await getEnvironmentSuffix()}.iqmetrix.net/someEndpoint`;
const URL = "https://jsonplaceholder.typicode.com/posts";
export const PostsTable = () => {
// As your application becomes more complex, you will want to consider factoring this into a
// hook and likely useReducer as well.
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | undefined>();
const [posts, setPosts] = useState<Array<Post> | undefined>();
useEffect(() => {
// useEffect does not allow async functions, so you must have it wrap the fetching logic.
const doFetch = async () => {
// Set the loading state.
setIsLoading(true);
try {
// authenticatedFetch functions exactly like the standard fetch function but injects
// the 'Authorization' header with the user's token.
const res = await authenticatedFetch(URL);
// If the response succeeds but returns a status you consider a failure, throw an error
// Here we are throwing the response status in an error.
if (!res.ok) throw new Error(res.status.toString());
// Convert the response to json and cast it to the expected type.
// You may further validate the response structure here.
const results = await res.json() as Array<Post>;
// Store your resource in state to trigger a rerender.
setPosts(results)
} catch (error) {
setError(error.toString());
} finally {
// Always unset the loading state.
setIsLoading(false);
}
};
doFetch();
}, []);
// PageLayout takes an object as a child and places the attributes in associated slots.
// Displays a table with the fetched resources.
return (
<PageLayout size="full">
{{
header: <PageHeader title="Display a table of resources" />,
messages: (error ? <Alert message={error} /> : null),
content: [
{
primary: (
<Table
bordered
loading={isLoading}
dataSource={posts}
columns={[
{
title: "ID",
dataIndex: "id",
key: "id",
},
{
title: "title",
dataIndex: "title",
key: "title",
},
]}
/>
),
},
],
}}
</PageLayout>
);
};

Editing a single resource#

import React, { useEffect, useState } from "react";
import { useParams } from 'react-router-dom';
// Function packages
import authenticatedFetch from "@iqmetrix/authenticated-fetch";
// Component packages
import { PageLayout } from "@iqmetrix/layout";
import { Alert, Button, PageHeader, Form, Input, Select, Skeleton } from "@iqmetrix/antd";
// Define the structure of your data model.
// You will want to define this within its own file to share across your application code.
interface Post {
userId: number;
id: number;
title: string;
body: string;
}
interface User {
id: string;
name: string;
}
// Define the URL you would like to fetch from.
// You will want to define this within its own file to share across your application.
// You can use a function when you need to calculate pieces of the URL.
// getEnvironmentSuffix() is commonly used to interact with the proper environment's service
// E.g. cosnt URL = async () => `https://service${await getEnvironmentSuffix()}.iqmetrix.net/someEndpoint`;
const getPostUrl = (id: string) => `https://jsonplaceholder.typicode.com/posts/${id}`;
const usersUrl = `https://jsonplaceholder.typicode.com/users`;
// Hooks can be used to separate your applicaiton's logic from the views making it more composable and
// testable. These are simplified hooks and will want to either add more edgecases to them or switch to
// a library to handle more complex edge cases.
// Handle the state of requesting a list of users
const useUsers = () => {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | undefined>();
const [users, setUsers] = useState<Array<User> | undefined>();
useEffect(() => {
// useEffect does not allow async functions, so you must have it wrap the fetching logic.
const doFetch = async () => {
// Set the loading state.
setIsLoading(true);
try {
// authenticatedFetch functions exactly like the standard fetch function but injects
// the 'Authorization' header with the user's token.
const res = await authenticatedFetch(usersUrl);
// If the response succeeds but returns a status you consider a failure, throw an error
// Here we are throwing the response status in an error.
if (!res.ok) throw new Error(res.status.toString());
// Convert the response to json and cast it to the expected type.
// You may further validate the response structure here.
const results = await res.json() as Array<User>;
// Store your resource in state to trigger a rerender.
setUsers(results)
} catch (error) {
setError(error.toString());
} finally {
// Always unset the loading state.
setIsLoading(false);
}
};
doFetch();
}, []);
return {
isLoading,
error,
users,
}
};
// Handle the state of requesting and saving a post
const usePost = (id: string) => {
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | undefined>();
const [post, setPost] = useState<Post | undefined>();
const [isSaving, setIsSaving] = useState<boolean>(false);
useEffect(() => {
// useEffect does not allow async functions, so you must have it wrap the fetching logic.
const doFetch = async () => {
// Set the loading state.
setIsLoading(true);
try {
// authenticatedFetch functions exactly like the standard fetch function but injects
// the 'Authorization' header with the user's token.
const res = await authenticatedFetch(getPostUrl(id));
// If the response succeeds but returns a status you consider a failure, throw an error
// Here we are throwing the response status in an error.
if (!res.ok) throw new Error(res.status.toString());
// Convert the response to json and cast it to the expected type.
// You may further validate the response structure here.
const result = await res.json() as Post;
// Store your resource in state to trigger a rerender.
setPost(result)
} catch (error) {
setError(error.toString());
} finally {
// Always unset the loading state.
setIsLoading(false);
}
};
doFetch();
}, [id]);
// A function to save the edited resource
const savePost = async (post: Post) => {
// Set the saving state.
setIsSaving(true);
try {
// authenticatedFetch functions exactly like the standard fetch function but injects
// the 'Authorization' header with the user's token.
const res = await authenticatedFetch(getPostUrl(id), {
method: 'PUT',
body: JSON.stringify(post),
});
// If the response succeeds but returns a status you consider a failure, throw an error
// Here we are throwing the response status in an error.
if (!res.ok) throw new Error(res.status.toString());
alert(JSON.stringify(post, undefined, 2));
} catch (error) {
setError(error.toString());
} finally {
// Always unset the loading state.
setIsSaving(false);
}
};
return {
error,
isLoading,
isSaving,
post,
savePost,
}
};
export const PostView = () => {
// Assuming the page is routed to with an :id parameter
// <Route exact path="/:id" component={PostView} />
const { id } = useParams<{ id: string}>();
// Use the users hook
const {
error: usersError,
isLoading: isUsersLoading,
users,
} = useUsers();
// Use the post hook
const {
error: postError,
isLoading: isPostLoading,
isSaving,
post,
savePost,
} = usePost(id)
// Combine and filter out empty errors
const errors = [postError, usersError].filter(error => !!error);
// PageLayout takes an object as a child and places the attributes in associated slots.
// Show a skeleton until the resource has been loaded
return (
<PageLayout size="full">
{{
header: <PageHeader title={`Resource ${id}`} />,
messages: errors.map(error => <Alert message={error?.toString()} closable />),
content: [
{
primary: (
<Skeleton loading={isPostLoading} >
<Form
initialValues={post}
layout="vertical"
name="post"
onFinish={savePost}
>
<Form.Item
label="Title"
name="title"
rules={[
{
required: true,
message: "You must enter a title"
}
]}
>
<Input />
</Form.Item>
<Form.Item
label="By"
name="userId"
rules={[
{
required: true,
message: "Your post must have an author"
}
]}
>
<Select loading={isUsersLoading}>
{users && users.map(user => (
<Select.Option value={user.id} title={user.name} key={user.id}>
{user.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item label="Body" name="body" >
<Input.TextArea rows={10} />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={isSaving} >
Save
</Button>
</Form.Item>
</Form>
</Skeleton>
),
},
],
}}
</PageLayout>
);
};
Last updated on by Garrett Smith