#generatetokens
Explore tagged Tumblr posts
digitaji · 4 months ago
Text
How to Create a Token on BASE with DeployTokens
Tumblr media
The BASE blockchain has become a popular choice for developers looking to create scalable and low-cost tokens. Whether you're building a meme coin, utility token, or governance asset, BASE provides the ideal environment for launching and managing digital assets. In this guide, we’ll walk you through the process of creating a BASE token using DeployTokens, ensuring a seamless and secure token creation experience.
Why Choose BASE for Token Creation?
BASE is a Layer 2 blockchain built on Ethereum, offering several advantages for token creators:
Low Transaction Fees – Reduced gas costs compared to Ethereum, making token transactions affordable.
Scalability – Faster transaction processing with high network efficiency.
EVM Compatibility – Supports Ethereum-based smart contracts, allowing easy deployment.
Growing Ecosystem – Increasing adoption in DeFi, NFTs, and Web3 applications.
With BASE’s efficiency and DeployTokens’ user-friendly platform, launching a token has never been easier.
Key Features of a BASE Token
Before diving into the creation process, it's essential to understand the features available for BASE tokens:
Minting & Burning – Define whether tokens can be minted or burned.
Supply Cap – Set a maximum token supply to prevent inflation.
Pause Functionality – Temporarily halt token transfers in case of an emergency.
Liquidity Locking – Prevents liquidity removal, reducing rug-pull risks.
Customizable Fees – Implement transaction taxes, rewards, or redistribution mechanisms.
Step-by-Step Guide to Creating a BASE Token with DeployTokens
Tumblr media
Step 1: Connect Your Wallet
To get started, visit DeployTokens.co and connect your Ethereum-compatible wallet, such as:
MetaMask
Trust Wallet
Coinbase Wallet
Ensure your wallet is funded with ETH for transaction fees.
Step 2: Choose Token Type & Enter Details
Once connected, select BASE as your blockchain network. Then, provide key token details:
Token Name (e.g., "MyBaseToken")
Symbol (e.g., "MBT")
Total Supply (e.g., 1,000,000 MBT)
Decimals (typically 18 for ERC-20 tokens)
Step 3: Customize Token Features
DeployTokens offers optional settings to enhance security and functionality:
Enable Minting/Burning – Allow future token supply adjustments.
Set Supply Cap – Prevent the creation of additional tokens beyond a fixed limit.
Pause Transfers – Enable an emergency stop mechanism.
Liquidity Locking – Prevents the removal of liquidity from decentralized exchanges.
Custom Fees – Implement transaction tax features.
Step 4: Deploy Your Token
After reviewing the settings, click “Deploy Token” to confirm the transaction. DeployTokens automatically generates and verifies your smart contract on BASE, ensuring transparency and security.
Step 5: Add Liquidity & List Your Token
Once deployed, you’ll need to distribute and trade your token:
Add Liquidity – Provide liquidity on decentralized exchanges (DEXs) like Uniswap.
List on Marketplaces – Submit your token for listing on CoinGecko, CoinMarketCap, or DexTools.
Engage with Community – Promote your token through social media and DeFi platforms.
Why Use DeployTokens for BASE Token Creation?
DeployTokens simplifies token deployment with built-in security measures, making it the best choice for launching BASE tokens. Key benefits include:
Automatic Smart Contract Verification – Ensures contract security and transparency.
No Coding Required – User-friendly interface for effortless token creation.
Liquidity & Security Features – Reduces risks of rug pulls and exploits.
Seamless Wallet Integration – Connect and launch with trusted Web3 wallets.
Final Thoughts
Creating a BASE token is straightforward with DeployTokens, offering security, scalability, and transparency. Whether you’re launching a DeFi token, a play-to-earn asset, or a community-driven meme coin, BASE provides the perfect foundation.
🚀 Ready to launch your BASE token? Get started today with DeployTokens.co!
0 notes
learning-code-ficusoft · 4 months ago
Text
Provide insights into securing Java web and desktop applications.
Tumblr media
Securing Java web and desktop applications requires a combination of best practices, security libraries, and frameworks to prevent vulnerabilities like SQL injection, XSS, CSRF, and unauthorized access. Here’s a deep dive into key security measures:
1. Secure Authentication and Authorization
Use Strong Authentication Mechanisms
Implement OAuth 2.0, OpenID Connect, or SAML for authentication.
Use Spring Security for web applications.
Enforce multi-factor authentication (MFA) for added security.
Example (Spring Security Basic Authentication in Java Web App)java@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated()) .httpBasic(); return http.build(); } }Implement Role-Based Access Control (RBAC)
Define roles and permissions for users.
Use JWT (JSON Web Tokens) for securing APIs.
Example (Securing API using JWT in Spring Boot)javapublic class JwtUtil { private static final String SECRET_KEY = "secureKey"; public String generateToken(String username) { return Jwts.builder() .setSubject(username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } }
2. Secure Data Storage and Transmission
Use Secure Communication (HTTPS & TLS)
Use TLS 1.2+ for encrypting data in transit.
Enforce HSTS (HTTP Strict Transport Security).
Encrypt Sensitive Data
Store passwords using bcrypt, PBKDF2, or Argon2.
Use AES-256 for encrypting sensitive data.
Example (Hashing Passwords in Java)javaimport org.mindrot.jbcrypt.BCrypt;public class PasswordSecurity { public static String hashPassword(String password) { return BCrypt.hashpw(password, BCrypt.gensalt(12)); } public static boolean verifyPassword(String password, String hashedPassword) { return BCrypt.checkpw(password, hashedPassword); } }
Use Secure Database Connections
Use parameterized queries to prevent SQL injection.
Disable database user permissions that are not required.
Example (Using Prepared Statements in JDBC)javaPreparedStatement stmt = connection.prepareStatement("SELECT * FROM users WHERE username = ?"); stmt.setString(1, username); ResultSet rs = stmt.executeQuery();
3. Protect Against Common Web Vulnerabilities
Prevent SQL Injection
Always use ORM frameworks (Hibernate, JPA) to manage queries securely.
Mitigate Cross-Site Scripting (XSS)
Escape user input in web views using OWASP Java Encoder.
Use Content Security Policy (CSP) headers.
Prevent Cross-Site Request Forgery (CSRF)
Use CSRF tokens in forms.
Enable CSRF protection in Spring Security.
Example (Enabling CSRF Protection in Spring Security)javahttp.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
4. Secure File Uploads and Deserialization
Validate File Uploads
Restrict allowed file types (e.g., only images, PDFs).
Use virus scanning (e.g., ClamAV).
Example (Checking File Type in Java)javaif (!file.getContentType().equals("application/pdf")) { throw new SecurityException("Invalid file type"); }
Avoid Untrusted Deserialization
Use whitelisting for allowed classes.
Prefer JSON over Java serialization.
Example (Disable Unsafe Object Deserialization in Java)javaObjectInputStream ois = new ObjectInputStream(inputStream) { @Override protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { throw new InvalidClassException("Deserialization is not allowed"); } };
5. Secure Desktop Java Applications
Use Code Signing
Sign JAR files using Java Keytool to prevent tampering.
shjarsigner -keystore mykeystore.jks -signedjar SecureApp.jar MyApp.jar myaliasRestrict JavaFX/Swing Application Permissions
Use Java Security Manager (deprecated but useful for legacy apps).
Restrict access to file system, network, and system properties.
Encrypt Local Data Storage
Use AES encryption for storing local files.
Example (Encrypting Files with AES in Java)javaCipher cipher = Cipher.getInstance("AES"); cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES")); byte[] encrypted = cipher.doFinal(data);
6. Logging and Monitoring for Security
Use Secure Logging Frameworks
Use logback or SLF4J.
Avoid logging sensitive data like passwords.
Monitor for Anomalies
Implement Intrusion Detection Systems (IDS).
Use audit trails and security alerts.
7. Best Practices for Securing Java Applications
✅ Keep dependencies up to date (Use OWASP Dependency Check). ✅ Run security scans (SAST, DAST) using SonarQube, Checkmarx. ✅ Apply the principle of least privilege for database and API access. ✅ Enforce strong password policies (min length, special characters). ✅ Use API Gateway and rate limiting for public-facing APIs.
Conclusion
Securing Java web and desktop applications requires multi-layered security across authentication, data protection, and vulnerability mitigation. By following best practices like strong encryption, secure coding techniques, and continuous monitoring, developers can protect applications against cyber threats.
WEBSITE: https://www.ficusoft.in/core-java-training-in-chennai/
0 notes
masaa-ma · 6 years ago
Text
PlantUMLによってコードベースでAWSのアーキテクチャー図を作る方法
from https://qiita.com/munieru_jp/items/088dfc3e5e91b5ea17c3?utm_campaign=popular_items&utm_medium=feed&utm_source=popular_items
AWS上にサービスを構築するうえで、アーキテクチャー図を作る機会はままあるかと思います。 その際、draw.ioやCacooなどのウェブサービスで作っている人も多いのではないでしょうか。 今回は別のアプローチとして、PlantUMLによってコードベースでAWSのアーキテクチャー図を作る方法をご紹介します。
PlantUMLの実行環境を用意
まずは、PlantUMLの実行環境を用意します。 ローカル環境にインストールするのもいいですが、素早く試したい場合はPlantUML Web Serverを使うのが便利です。
AWSのアイコンセットを用意
PlantUMLでは、ファイルパスやURLを指定してリソースをインポートすることができます。 これにより自作の画像を組み込むことができるわけですが、ありがたいことにAWSが公式にPlantUMLのためのアイコンセットを配布しています。
記事執筆��点でスター数が57と、まだ世の中にはあまり知られていないようです。
PlantUMLのコードを書く
さっそくですが、次のようなコードを書いてみましょう。
@startuml Two Modes - Technical View !define AWSPuml https://raw.githubusercontent.com/awslabs/aws-icons-for-plantuml/master/dist !include AWSPuml/AWSCommon.puml !include AWSPuml/General/Users.puml !include AWSPuml/Mobile/APIGateway.puml !include AWSPuml/SecurityIdentityAndCompliance/Cognito.puml !include AWSPuml/Compute/Lambda.puml !include AWSPuml/Database/DynamoDB.puml left to right direction Users(sources, "Events", "millions of users") APIGateway(votingAPI, "Voting API", "user votes") Cognito(userAuth, "User Authentication", "jwt to submit votes") Lambda(generateToken, "User Credentials", "return jwt") Lambda(recordVote, "Record Vote", "enter or update vote per user") DynamoDB(voteDb, "Vote Database", "one entry per user") sources --> userAuth sources --> votingAPI userAuth <--> generateToken votingAPI --> recordVote recordVote --> voteDb @enduml
出典:https://github.com/awslabs/aws-icons-for-plantuml/blob/master/README.md
すると、このような図が生成されます。
それぞれのコードの意味を紐解いていきます。
AWSのアイコンをインポート
!define AWSPuml https://raw.githubusercontent.com/awslabs/aws-icons-for-plantuml/master/dist !include AWSPuml/AWSCommon.puml !include AWSPuml/General/Users.puml !include AWSPuml/Mobile/APIGateway.puml !include AWSPuml/SecurityIdentityAndCompliance/Cognito.puml !include AWSPuml/Compute/Lambda.puml !include AWSPuml/Database/DynamoDB.puml
使用するAWSのアイコンをインポートしています。 ここではGitHub上のURLを指定していますが、ローカルにダウンロードしてファイルパスを指定してもかまいません。
!define AWSPuml path/to/AWSPuml
描画方向を指定
図の描画方向を左から右に指定しています。 デフォルトでは上から下なので、この行を書かなければ次のようになります。
AWSのリソースを定義
Users(sources, "Events", "millions of users") APIGateway(votingAPI, "Voting API", "user votes") Cognito(userAuth, "User Authentication", "jwt to submit votes") Lambda(generateToken, "User Credentials", "return jwt") Lambda(recordVote, "Record Vote", "enter or update vote per user") DynamoDB(voteDb, "Vote Database", "one entry per user")
配置するAWSのリソースを定義しています。 名前の部分は、実際のリソース名にすると分かりやすいでしょう。
各リソースの関係を定義
sources --> userAuth sources --> votingAPI userAuth <--> generateToken votingAPI --> recordVote recordVote --> voteDb
それぞれのリソースの関係を定義しています。 リソース間を-->や<--で繋ぐと一方向、<-->で繋ぐと双方向の矢印が描画されます。
生成した図を埋め込む
Qiitaの記事中にPlantUMLの図を埋め込む場合、UML anywhereというChrome拡張を使用すると便利です。1 なお、Qiita:TeamではPlantUMLの埋め込みに対応しているので、コードブロックの言語にplantumlを指定することで図が生成されます。2
おわりに
CloudFormationのテンプレートを読み込んで、PlantUML用のコードを出力してくれるようなツールがあれば最高ですね。3
https://cdn.qiita.com/assets/qiita-fb-fe28c64039d925349e620ba55091e078.png
0 notes
laurelkrugerr · 5 years ago
Text
Setting Up Redux For Use In A Real-World Application
About The Author
I love building software for the web, writing about web technologies, and playing video games. More about Jerry …
RedUX is a robust state-management library for single-page Javascript apps. It is described on the official documentation as a predictable state container for Javascript applications and it’s fairly simple to learn the concepts and implement RedUX in a simple app. Going from a simple counter app to a real-world app, however, can be quite the jump.
RedUX is an important library in the React ecosystem, and almost the default to use when working on React applications that involve state management. As such, the importance of knowing how it works cannot be overestimated.
This guide will walk the reader through setting up RedUX in a fairly complex React application and introduce the reader to “best practices” configuration along the way. It will be beneficial to beginners especially, and anyone who wants to fill in the gaps in their knowledge of RedUX.
Introducing Redux
RedUX is a library that aims to solve the problem of state management in JavaScript apps by imposing restrictions on how and when state updates can happen. These restrictions are formed from RedUX’s “three principles” which are:
Single source of truth All of your application’s state is held in a RedUX store. This state can be represented visually as a tree with a single ancestor, and the store provides methods for reading the current state and subscribing to changes from anywhere within your app.
State is read-only The only way to change the state is to send the data as a plain object, called an action. You can think about actions as a way of saying to the state, “I have some data I would like to insert/update/delete”.
Changes are made with pure functions To change your app’s state, you write a function that takes the previous state and an action and returns a new state object as the next state. This function is called a reducer, and it is a pure function because it returns the same output for a given set of inputs.
The last principle is the most important in RedUX, and this is where the magic of RedUX happens. Reducer functions must not contain unpredictable code, or perform side-effects such as network requests, and should not directly mutate the state object.
RedUX is a great tool, as we’ll learn later in this guide, but it doesn’t come without its challenges or tradeoffs. To help make the process of writing RedUX efficient and more enjoyable, the RedUX team offers a toolkit that abstracts over the process of setting up a RedUX store and provides helpful RedUX add-ons and utilities that help to simplify application code. For example, the library uses Immer.js, a library that makes it possible for you to write “mutative” immutable update logic, under the hood.
Recommended reading: Better Reducers With Immer
In this guide, we will explore RedUX by building an application that lets authenticated users create and manage digital diaries.
Building Diaries.app
As stated in the previous section, we will be taking a closer look at RedUX by building an app that lets users create and manage diaries. We will be building our application using React, and we’ll set up Mirage as our API mocking server since we won’t have access to a real server in this guide.
Starting a Project and Installing Dependencies
Let’s get started on our project. First, bootstrap a new React application using create-react-app:
Using npx:
npx create-react-app diaries-app --template typescript
We are starting with the TypeScript template, as we can improve our development experience by writing type-safe code.
Now, let’s install the dependencies we’ll be needing. Navigate into your newly created project directory
cd diaries-app
And run the following commands:
npm install --save redUX react-redUX @redUXjs/toolkit
npm install --save axios react-router-dom react-hook-form yup dayjs markdown-to-jsx sweetalert2
npm install --save-dev miragejs @types/react-redUX @types/react-router-dom @types/yup @types/markdown-to-jsx
The first command will install RedUX, React-RedUX (official React bindings for RedUX), and the RedUX toolkit.
The second command installs some extra packages which will be useful for the app we’ll be building but are not required to work with RedUX.
The last command installs Mirage and type declarations for the packages we installed as devDependencies.
Describing the Application’s Initial State
Let’s go over our application’s requirements in detail. The application will allow authenticated users to create or modify existing diaries. Diaries are private by default, but they can be made public. Finally, diary entries will be sorted by their last modified date.
This relationship should look something like this:
An Overview of the Application’s Data Model. (Large preview)
Armed with this information, we can now model our application’s state. First, we will create an interface for each of the following resources: User, Diary and DiaryEntry. Interfaces in Typescript describe the shape of an object.
Go ahead and create a new directory named interfaces in your app’s src sub-directory:
cd src && mkdir interfaces
Next, run the following commands in the directory you just created:
touch entry.interface.ts touch diary.interface.ts touch user.interface.ts
This will create three files named entry.interface.ts, diary.interface.ts and user.interface.ts respectively. I prefer to keep interfaces that would be used in multiple places across my app in a single location.
Open entry.interface.ts and add the following code to set up the Entry interface:
export interface Entry { id?: string; title: string; content: string; createdAt?: string; updatedAt?: string; diaryId?: string; }
A typical diary entry will have a title and some content, as well as information about when it was created or last updated. We’ll get back to the diaryId property later.
Next, add the following to diary.interface.ts:
export interface Diary { id?: string; title: string; type: 'private' | 'public'; createdAt?: string; updatedAt?: string; userId?: string; entryIds: string[] | null; }
Here, we have a type property which expects an exact value of either ‘private’ or ‘public’, as diaries must be either private or public. Any other value will throw an error in the TypeScript compiler.
We can now describe our User object in the user.interface.ts file as follows:
export interface User { id?: string; username: string; email: string; password?: string; diaryIds: string[] | null; }
With our type definitions finished and ready to be used across our app, let’s setup our mock API server using Mirage.
Setting up API Mocking with MirageJS
Since this tutorial is focused on RedUX, we will not go into the details of setting up and using Mirage in this section. Please check out this excellent series if you would like to learn more about Mirage.
To get started, navigate to your src directory and create a file named server.ts by running the following commands:
mkdir -p services/mirage cd services/mirage # ~/diaries-app/src/services/mirage touch server.ts
Next, open the server.ts file and add the following code:
import { Server, Model, Factory, belongsTo, hasMany, Response } from 'miragejs'; export const handleErrors = (error: any, message = 'An error ocurred') => { return new Response(400, undefined, { data: { message, isError: true, }, }); }; export const setupServer = (env?: string): Server => { return new Server({ environment: env ?? 'development', models: { entry: Model.extend({ diary: belongsTo(), }), diary: Model.extend({ entry: hasMany(), user: belongsTo(), }), user: Model.extend({ diary: hasMany(), }), }, factories: { user: Factory.extend({ username: 'test', password: 'password', email: '[email protected]', }), }, seeds: (server): any => { server.create('user'); }, routes(): void { this.urlPrefix = 'https://diaries.app'; }, }); };
In this file, we are exporting two functions. A utility function for handling errors, and setupServer(), which returns a new server instance. The setupServer() function takes an optional argument which can be used to change the server’s environment. You can use this to set up Mirage for testing later.
We have also defined three models in the server’s models property: User, Diary and Entry. Remember that earlier we set up the Entry interface with a property named diaryId. This value will be automatically set to the id the entry is being saved to. Mirage uses this property to establish a relationship between an Entry and a Diary. The same thing also happens when a user creates a new diary: userId is automatically set to that user’s id.
We seeded the database with a default user and configured Mirage to intercept all requests from our app starting with https://diaries.app. Notice that we haven’t configured any route handlers yet. Let’s go ahead and create a few.
Ensure that you are in the src/services/mirage directory, then create a new directory named routes using the following command:
# ~/diaries-app/src/services/mirage mkdir routes
cd to the newly created directory and create a file named user.ts:
cd routes touch user.ts
Next, paste the following code in the user.ts file:
import { Response, Request } from 'miragejs'; import { handleErrors } from '../server'; import { User } from '../../../interfaces/user.interface'; import { randomBytes } from 'crypto'; const generateToken = () => randomBytes(8).toString('hex'); export interface AuthResponse { token: string; user: User; } const login = (schema: any, req: Request): AuthResponse | Response => { const { username, password } = JSON.parse(req.requestBody); const user = schema.users.findBy({ username }); if (!user) { return handleErrors(null, 'No user with that username exists'); } if (password !== user.password) { return handleErrors(null, 'Password is incorrect'); } const token = generateToken(); return { user: user.attrs as User, token, }; }; const signup = (schema: any, req: Request): AuthResponse | Response => { const data = JSON.parse(req.requestBody); const exUser = schema.users.findBy({ username: data.username }); if (exUser) { return handleErrors(null, 'A user with that username already exists.'); } const user = schema.users.create(data); const token = generateToken(); return { user: user.attrs as User, token, }; }; export default { login, signup, };
The login and signup methods here receive a Schema class and a fake Request object and, upon validating the password or checking that the login does not already exist, return the existing user or a new user respectively. We use the Schema object to interact with Mirage’s ORM, while the Request object contains information about the intercepted request including the request body and headers.
Next, let’s add methods for working with diaries and diary entries. Create a file named diary.ts in your routes directory:
touch diary.ts
Update the file with the following methods for working with Diary resources:
export const create = ( schema: any, req: Request ): { user: User; diary: Diary } | Response => { try { const { title, type, userId } = JSON.parse(req.requestBody) as Partial< Diary >; const exUser = schema.users.findBy({ id: userId }); if (!exUser) { return handleErrors(null, 'No such user exists.'); } const now = dayjs().format(); const diary = exUser.createDiary({ title, type, createdAt: now, updatedAt: now, }); return { user: { ...exUser.attrs, }, diary: diary.attrs, }; } catch (error) { return handleErrors(error, 'Failed to create Diary.'); } }; export const updateDiary = (schema: any, req: Request): Diary | Response => { try { const diary = schema.diaries.find(req.params.id); const data = JSON.parse(req.requestBody) as Partial<Diary>; const now = dayjs().format(); diary.update({ ...data, updatedAt: now, }); return diary.attrs as Diary; } catch (error) { return handleErrors(error, 'Failed to update Diary.'); } }; export const getDiaries = (schema: any, req: Request): Diary[] | Response => { try { const user = schema.users.find(req.params.id); return user.diary as Diary[]; } catch (error) { return handleErrors(error, 'Could not get user diaries.'); } };
Next, let’s add some methods for working with diary entries:
export const addEntry = ( schema: any, req: Request ): { diary: Diary; entry: Entry } | Response => { try { const diary = schema.diaries.find(req.params.id); const { title, content } = JSON.parse(req.requestBody) as Partial<Entry>; const now = dayjs().format(); const entry = diary.createEntry({ title, content, createdAt: now, updatedAt: now, }); diary.update({ ...diary.attrs, updatedAt: now, }); return { diary: diary.attrs, entry: entry.attrs, }; } catch (error) { return handleErrors(error, 'Failed to save entry.'); } }; export const getEntries = ( schema: any, req: Request ): { entries: Entry[] } | Response => { try { const diary = schema.diaries.find(req.params.id); return diary.entry; } catch (error) { return handleErrors(error, 'Failed to get Diary entries.'); } }; export const updateEntry = (schema: any, req: Request): Entry | Response => { try { const entry = schema.entries.find(req.params.id); const data = JSON.parse(req.requestBody) as Partial<Entry>; const now = dayjs().format(); entry.update({ ...data, updatedAt: now, }); return entry.attrs as Entry; } catch (error) { return handleErrors(error, 'Failed to update entry.'); } };
Finally, let’s add the necessary imports at the top of the file:
import { Response, Request } from 'miragejs'; import { handleErrors } from '../server'; import { Diary } from '../../../interfaces/diary.interface'; import { Entry } from '../../../interfaces/entry.interface'; import dayjs from 'dayjs'; import { User } from '../../../interfaces/user.interface';
In this file, we have exported methods for working with the Diary and Entry models. In the create method, we call a method named user.createDiary() to save a new diary and associate it to a user account.
The addEntry and updateEntry methods create and correctly associate a new entry to a diary or update an existing entry’s data respectively. The latter also updates the entry’s updatedAt property with the current timestamp. The updateDiary method also updates a diary with the timestamp the change was made. Later, we’ll be sorting the records we receive from our network request with this property.
We also have a getDiaries method which retrieves a user’s diaries and a getEntries methods which retrieves a selected diary’s entries.
We can now update our server to use the methods we just created. Open server.ts to include the files:
import { Server, Model, Factory, belongsTo, hasMany, Response } from 'miragejs'; import user from './routes/user'; import * as diary from './routes/diary';
Then, update the server’s route property with the routes we want to handle:
export const setupServer = (env?: string): Server => { return new Server({ // ... routes(): void { this.urlPrefix = 'https://diaries.app'; this.get('/diaries/entries/:id', diary.getEntries); this.get('/diaries/:id', diary.getDiaries); this.post('/auth/login', user.login); this.post('/auth/signup', user.signup); this.post('/diaries/', diary.create); this.post('/diaries/entry/:id', diary.addEntry); this.put('/diaries/entry/:id', diary.updateEntry); this.put('/diaries/:id', diary.updateDiary); }, }); };
With this change, when a network request from our app matches one of the route handlers, Mirage intercepts the request and invokes the respective route handler functions.
Next, we’ll proceed to make our application aware of the server. Open src/index.tsx and import the setupServer() method:
import { setupServer } from './services/mirage/server';
And add the following code before ReactDOM.render():
if (process.env.NODE_ENV === 'development') { setupServer(); }
The check in the code block above ensures that our Mirage server will run only while we are in development mode.
One last thing we need to do before moving on to the RedUX bits is configure a custom Axios instance for use in our app. This will help to reduce the amount of code we’ll have to write later on.
Create a file named api.ts under src/services and add the following code to it:
import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios'; import { showAlert } from '../util'; const http: AxiosInstance = axios.create({ baseURL: 'https://diaries.app', }); http.defaults.headers.post['Content-Type'] = 'application/json'; http.interceptors.response.use( async (response: AxiosResponse): Promise => { if (response.status >= 200 && response.status < 300) { return response.data; } }, (error: AxiosError) => { const { response, request }: { response?: AxiosResponse; request?: XMLHttpRequest; } = error; if (response) { if (response.status >= 400 && response.status < 500) { showAlert(response.data?.data?.message, 'error'); return null; } } else if (request) { showAlert('Request failed. Please try again.', 'error'); return null; } return Promise.reject(error); } ); export default http;
In this file, we are exporting an Axios instance modified to include our app’s API url, https://diaries.app. We have configured an interceptor to handle success and error responses, and we display error messages using a sweetalert toast which we will configure in the next step.
Create a file named util.ts in your src directory and paste the following code in it:
import Swal, { SweetAlertIcon } from 'sweetalert2'; export const showAlert = (titleText = 'Something happened.', alertType?: SweetAlertIcon): void => { Swal.fire({ titleText, position: 'top-end', timer: 3000, timerProgressBar: true, toast: true, showConfirmButton: false, showCancelButton: true, cancelButtonText: 'Dismiss', icon: alertType, showClass: { popup: 'swal2-noanimation', backdrop: 'swal2-noanimation', }, hideClass: { popup: '', backdrop: '', }, }); };
This file exports a function that displays a toast whenever it is invoked. The function accepts parameters to allow you set the toast message and type. For example, we are showing an error toast in the Axios response error interceptor like this:
showAlert(response.data?.data?.message, 'error');
Now when we make requests from our app while in development mode, they will be intercepted and handled by Mirage instead. In the next section, we will set up our RedUX store using RedUX toolkit.
Setting up a Redux Store
In this section, we are going to set up our store using the following exports from RedUX toolkit: configureStore(), getDefaultMiddleware() and createSlice(). Before we start, we should take a detailed look at what these exports do.
configureStore() is an abstraction over the RedUX createStore() function that helps simplify your code. It uses createStore() internally to set up your store with some useful development tools:
export const store = configureStore({ reducer: rootReducer, // a single reducer function or an object of slice reducers });
The createSlice() function helps simplify the process of creating action creators and slice reducers. It accepts an initial state, an object full of reducer functions, and a “slice name”, and automatically generates action creators and action types corresponding to the reducers and your state. It also returns a single reducer function, which can be passed to RedUX’s combineReducers() function as a “slice reducer”.
Remember that the state is a single tree, and a single root reducer manages changes to that tree. For maintainability, it is recommended to split your root reducer into “slices,” and have a “slice reducer” provide an initial value and calculate the updates to a corresponding slice of the state. These slices can be joined into a single reducer function by using combineReducers().
There are additional options for configuring the store. For example, you can pass an array of your own middleware to configureStore() or start up your app from a saved state using the preloadedState option. When you supply the middleware option, you have to define all the middleware you want added to the store. If you would like to retain the defaults when setting up your store, you can use getDefaultMiddleware() to get the default list of middleware:
export const store = configureStore({ // ... middleware: [...getDefaultMiddleware(), customMiddleware], });
Let’s now proceed to set up our store. We will adopt a “ducks-style” approach to structuring our files, specifically following the guidelines in practice from the Github Issues sample app. We will be organizing our code such that related components, as well as actions and reducers, live in the same directory. The final state object will look like this:
type RootState = { auth: { token: string | null; isAuthenticated: boolean; }; diaries: Diary[]; entries: Entry[]; user: User | null; editor: { canEdit: boolean; currentlyEditing: Entry | null; activeDiaryId: string | null; }; }
To get started, create a new directory named features under your src directory:
# ~/diaries-app/src mkdir features
Then, cd into features and create directories named auth, diary and entry:
cd features mkdir auth diary entry
cd into the auth directory and create a file named authSlice.ts:
cd auth # ~/diaries-app/src/features/auth touch authSlice.ts
Open the file and paste the following in it:
import { createSlice, PayloadAction } from '@redUXjs/toolkit'; interface AuthState { token: string | null; isAuthenticated: boolean; } const initialState: AuthState = { token: null, isAuthenticated: false, }; const auth = createSlice({ name: 'auth', initialState, reducers: { saveToken(state, { payload }: PayloadAction) { if (payload) { state.token = payload; } }, clearToken(state) { state.token = null; }, setAuthState(state, { payload }: PayloadAction) { state.isAuthenticated = payload; }, }, }); export const { saveToken, clearToken, setAuthState } = auth.actions; export default auth.reducer;
In this file, we’re creating a slice for the auth property of our app’s state using the createSlice() function introduced earlier. The reducers property holds a map of reducer functions for updating values in the auth slice. The returned object contains automatically generated action creators and a single slice reducer. We would need to use these in other files so, following the “ducks pattern”, we do named exports of the action creators, and a default export of the reducer function.
Let’s set up the remaining reducer slices according to the app state we saw earlier. First, create a file named userSlice.ts in the auth directory and add the following code to it:
import { createSlice, PayloadAction } from '@redUXjs/toolkit'; import { User } from '../../interfaces/user.interface'; const user = createSlice({ name: 'user', initialState: null as User | null, reducers: { setUser(state, { payload }: PayloadAction<User | null>) { return state = (payload != null) ? payload : null; }, }, }); export const { setUser } = user.actions; export default user.reducer;
This creates a slice reducer for the user property in our the application’s store. The setUser reducer function accepts a payload containing user data and updates the state with it. When no data is passed, we set the state’s user property to null.
Next, create a file named diariesSlice.ts under src/features/diary:
# ~/diaries-app/src/features cd diary touch diariesSlice.ts
Add the following code to the file:
import { createSlice, PayloadAction } from '@redUXjs/toolkit'; import { Diary } from '../../interfaces/diary.interface'; const diaries = createSlice({ name: 'diaries', initialState: [] as Diary[], reducers: { addDiary(state, { payload }: PayloadAction<Diary[]>) { const diariesToSave = payload.filter((diary) => { return state.findIndex((item) => item.id === diary.id) === -1; }); state.push(...diariesToSave); }, updateDiary(state, { payload }: PayloadAction<Diary>) { const { id } = payload; const diaryIndex = state.findIndex((diary) => diary.id === id); if (diaryIndex !== -1) { state.splice(diaryIndex, 1, payload); } }, }, }); export const { addDiary, updateDiary } = diaries.actions; export default diaries.reducer;
The “diaries” property of our state is an array containing the user���s diaries, so our reducer functions here all work on the state object they receive using array methods. Notice here that we are writing normal “mutative” code when working on the state. This is possible because the reducer functions we create using the createSlice() method are wrapped with Immer’s produce() method. This results in Immer returning a correct immutably updated result for our state regardless of us writing mutative code.
Next, create a file named entriesSlice.ts under src/features/entry:
# ~/diaries-app/src/features mkdir entry cd entry touch entriesSlice.ts
Open the file and add the following code:
import { createSlice, PayloadAction } from '@redUXjs/toolkit'; import { Entry } from '../../interfaces/entry.interface'; const entries = createSlice({ name: 'entries', initialState: [] as Entry[], reducers: { setEntries(state, { payload }: PayloadAction<Entry[] | null>) { return (state = payload != null ? payload : []); }, updateEntry(state, { payload }: PayloadAction<Entry>) { const { id } = payload; const index = state.findIndex((e) => e.id === id); if (index !== -1) { state.splice(index, 1, payload); } }, }, }); export const { setEntries, updateEntry } = entries.actions; export default entries.reducer;
The reducer functions here have logic similar to the previous slice’s reducer functions. The entries property is also an array, but it only holds entries for a single diary. In our app, this will be the diary currently in the user’s focus.
Finally, create a file named editorSlice.ts in src/features/entry and add the following to it:
import { createSlice, PayloadAction } from '@redUXjs/toolkit'; import { Entry } from '../../interfaces/entry.interface'; interface EditorState { canEdit: boolean; currentlyEditing: Entry | null; activeDiaryId: string | null; } const initialState: EditorState = { canEdit: false, currentlyEditing: null, activeDiaryId: null, }; const editor = createSlice({ name: 'editor', initialState, reducers: { setCanEdit(state, { payload }: PayloadAction<boolean>) { state.canEdit = payload != null ? payload : !state.canEdit; }, setCurrentlyEditing(state, { payload }: PayloadAction<Entry | null>) { state.currentlyEditing = payload; }, setActiveDiaryId(state, { payload }: PayloadAction<string>) { state.activeDiaryId = payload; }, }, }); export const { setCanEdit, setCurrentlyEditing, setActiveDiaryId } = editor.actions; export default editor.reducer;
Here, we have a slice for the editor property in state. We’ll be using the properties in this object to check if the user wants to switch to editing mode, which diary the edited entry belongs to, and what entry is going to be edited.
To put it all together, create a file named rootReducer.ts in the src directory with the following content:
import { combineReducers } from '@redUXjs/toolkit'; import authReducer from './features/auth/authSlice'; import userReducer from './features/auth/userSlice'; import diariesReducer from './features/diary/diariesSlice'; import entriesReducer from './features/entry/entriesSlice'; import editorReducer from './features/entry/editorSlice'; const rootReducer = combineReducers({ auth: authReducer, diaries: diariesReducer, entries: entriesReducer, user: userReducer, editor: editorReducer, }); export type RootState = ReturnType<typeof rootReducer>; export default rootReducer;
In this file, we’ve combined our slice reducers into a single root reducer with the combineReducers() function. We’ve also exported the RootState type, which will be useful later when we’re selecting values from the store. We can now use the root reducer (the default export of this file) to set up our store.
Create a file named store.ts with the following contents:
import { configureStore } from '@redUXjs/toolkit'; import rootReducer from './rootReducer'; import { useDispatch } from 'react-redUX'; const store = configureStore({ reducer: rootReducer, }); type AppDispatch = typeof store.dispatch; export const useAppDispatch = () => useDispatch<AppDispatch>(); export default store;
With this, we’ve created a store using the configureStore() export from RedUX toolkit. We’ve also exported an hook called useAppDispatch() which merely returns a typed useDispatch() hook.
Next, update the imports in index.tsx to look like the following:
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './app/App'; import * as serviceWorker from './serviceWorker'; import { setupServer } from './services/mirage/server'; import { Provider } from 'react-redUX'; import store from './store'; // ...
Finally, make the store available to the app’s components by wrapping <App /> (the top-level component) with <Provider />:
ReactDOM.render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode>, document.getElementById('root') );
Now, if you start your app and you navigate to http://localhost:3000 with the Redux Dev Tools extension enabled, you should see the following in your app’s state:
Initial State in RedUX Dev Tools Extension. (Large preview)
Great work so far, but we’re not quite finished yet. In the next section, we will design the app’s User Interface and add functionality using the store we’ve just created.
Designing The Application User Interface
To see RedUX in action, we are going to build a demo app. In this section, we will connect our components to the store we’ve created and learn to dispatch actions and modify the state using reducer functions. We will also learn how to read values from the store. Here’s what our RedUX-powered application will look like.
Home page showing an authenticated user’s diaries. (Large preview)
Screenshots of final app. (Large preview)
Setting up the Authentication Feature
To get started, move App.tsx and its related files from the src directory to its own directory like this:
# ~/diaries-app/src mkdir app mv App.tsx App.test.tsx app
You can delete the App.css and logo.svg files as we won’t be needing them.
Next, open the App.tsx file and replace its contents with the following:
import React, { FC, lazy, Suspense } from 'react'; import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; import { useSelector } from 'react-redUX'; import { RootState } from '../rootReducer'; const Auth = lazy(() => import('../features/auth/Auth')); const Home = lazy(() => import('../features/home/Home')); const App: FC = () => { const isLoggedIn = useSelector( (state: RootState) => state.auth.isAuthenticated ); return ( <Router> <Switch> <Route path="/"> <Suspense fallback={<p>Loading...</p>}> {isLoggedIn ? <Home /> : <Auth />} </Suspense> </Route> </Switch> </Router> ); }; export default App;
Here we have set up our app to render an <Auth /> component if the user is unauthenticated, or otherwise render a <Home /> component. We haven’t created either of these components yet, so let’s fix that. Create a file named Auth.tsx under src/features/auth and add the following contents to the file:
import React, { FC, useState } from 'react'; import { useForm } from 'react-hook-form'; import { User } from '../../interfaces/user.interface'; import * as Yup from 'yup'; import http from '../../services/api'; import { saveToken, setAuthState } from './authSlice'; import { setUser } from './userSlice'; import { AuthResponse } from '../../services/mirage/routes/user'; import { useAppDispatch } from '../../store'; const schema = Yup.object().shape({ username: Yup.string() .required('What? No username?') .max(16, 'Username cannot be longer than 16 characters'), password: Yup.string().required('Without a password, "None shall pass!"'), email: Yup.string().email('Please provide a valid email address ([email protected])'), }); const Auth: FC = () => { const { handleSubmit, register, errors } = useForm<User>({ validationSchema: schema, }); const [isLogin, setIsLogin] = useState(true); const [loading, setLoading] = useState(false); const dispatch = useAppDispatch(); const submitForm = (data: User) => { const path = isLogin ? '/auth/login' : '/auth/signup'; http .post<User, AuthResponse>(path, data) .then((res) => { if (res) { const { user, token } = res; dispatch(saveToken(token)); dispatch(setUser(user)); dispatch(setAuthState(true)); } }) .catch((error) => { console.log(error); }) .finally(() => { setLoading(false); }); }; return ( <div className="auth"> <div className="card"> <form onSubmit={handleSubmit(submitForm)}> <div className="inputWrapper"> <input ref={register} name="username" placeholder="Username" /> {errors && errors.username && ( <p className="error">{errors.username.message}</p> )} </div> <div className="inputWrapper"> <input ref={register} name="password" type="password" placeholder="Password" /> {errors && errors.password && ( <p className="error">{errors.password.message}</p> )} </div> {!isLogin && ( <div className="inputWrapper"> <input ref={register} name="email" placeholder="Email (optional)" /> {errors && errors.email && ( <p className="error">{errors.email.message}</p> )} </div> )} <div className="inputWrapper"> <button type="submit" disabled={loading}> {isLogin ? 'Login' : 'Create account'} </button> </div> <p onClick={() => setIsLogin(!isLogin)} style= > {isLogin ? 'No account? Create one' : 'Already have an account?'} </p> </form> </div> </div> ); }; export default Auth;
In this component, we have set up a form for users to log in, or to create an account. Our form fields are validated using Yup and, on successfully authenticating a user, we use our useAppDispatch hook to dispatch the relevant actions. You can see the dispatched actions and the changes made to your state in the RedUX DevTools Extension:
Dispatched Actions with Changes Tracked in RedUX Dev Tools Extensions. (Large preview)
Finally, create a file named Home.tsx under src/features/home and add the following code to the file:
import React, { FC } from 'react'; const Home: FC = () => { return ( <div> <p>Welcome user!</p> </div> ); }; export default Home;
For now, we are just displaying some text to the authenticated user. As we build the rest of our application, we will be updating this file.
Setting up the Editor
The next component we are going to build is the editor. Though basic, we will enable support for rendering markdown content using the markdown-to-jsx library we installed earlier.
First, create a file named Editor.tsx in the src/features/entry directory. Then, add the following code to the file:
import React, { FC, useState, useEffect } from 'react'; import { useSelector } from 'react-redUX'; import { RootState } from '../../rootReducer'; import Markdown from 'markdown-to-jsx'; import http from '../../services/api'; import { Entry } from '../../interfaces/entry.interface'; import { Diary } from '../../interfaces/diary.interface'; import { setCurrentlyEditing, setCanEdit } from './editorSlice'; import { updateDiary } from '../diary/diariesSlice'; import { updateEntry } from './entriesSlice'; import { showAlert } from '../../util'; import { useAppDispatch } from '../../store'; const Editor: FC = () => { const { currentlyEditing: entry, canEdit, activeDiaryId } = useSelector( (state: RootState) => state.editor ); const [editedEntry, updateEditedEntry] = useState(entry); const dispatch = useAppDispatch(); const saveEntry = async () => { if (activeDiaryId == null) { return showAlert('Please select a diary.', 'warning'); } if (entry == null) { http .post<Entry, { diary: Diary; entry: Entry }>( `/diaries/entry/${activeDiaryId}`, editedEntry ) .then((data) => { if (data != null) { const { diary, entry: _entry } = data; dispatch(setCurrentlyEditing(_entry)); dispatch(updateDiary(diary)); } }); } else { http .put<Entry, Entry>(`diaries/entry/${entry.id}`, editedEntry) .then((_entry) => { if (_entry != null) { dispatch(setCurrentlyEditing(_entry)); dispatch(updateEntry(_entry)); } }); } dispatch(setCanEdit(false)); }; useEffect(() => { updateEditedEntry(entry); }, [entry]); return ( <div className="editor"> <header style= > {entry && !canEdit ? ( <h4> {entry.title} <a href="#edit" onClick={(e) => { e.preventDefault(); if (entry != null) { dispatch(setCanEdit(true)); } }} style= > (Edit) </a> </h4> ) : ( <input value={editedEntry?.title ?? ''} disabled={!canEdit} onChange={(e) => { if (editedEntry) { updateEditedEntry({ ...editedEntry, title: e.target.value, }); } else { updateEditedEntry({ title: e.target.value, content: '', }); } }} /> )} </header> {entry && !canEdit ? ( <Markdown>{entry.content}</Markdown> ) : ( <> <textarea disabled={!canEdit} placeholder="Supports markdown!" value={editedEntry?.content ?? ''} onChange={(e) => { if (editedEntry) { updateEditedEntry({ ...editedEntry, content: e.target.value, }); } else { updateEditedEntry({ title: '', content: e.target.value, }); } }} /> <button onClick={saveEntry} disabled={!canEdit}> Save </button> </> )} </div> ); }; export default Editor;
Let’s break down what’s happening in the Editor component.
First, we are picking some values (with correctly inferred types) from the app’s state using the useSelector() hook from react-redUX. In the next line, we have a stateful value called editedEntry whose initial value is set to the editor.currentlyEditing property we’ve selected from the store.
Next, we have the saveEntry function which updates or creates a new entry in the API, and dispatches the respective RedUX action.
Finally, we have a useEffect that is fired when the editor.currentlyEditing property changes. Our editor’s UI (in the component’s return function) has been set up to respond to changes in the state. For example, rendering the entry’s content as JSX elements when the user isn’t editing.
With that, the app’s Entry feature should be completely set up. In the next section, we will finish building the Diary feature and then import the main components in the Home component we created earlier.
Final Steps
To finish up our app, we will first create components for the Diary feature. Then, we will update the Home component with the primary exports from the Diary and Entry features. Finally, we will add some styling to give our app the required pizzazz!
Let’s start by creating a file in src/features/diary named DiaryTile.tsx. This component will present information about a diary and its entries, and allow the user to edit the diary’s title. Add the following code to the file:
import React, { FC, useState } from 'react'; import { Diary } from '../../interfaces/diary.interface'; import http from '../../services/api'; import { updateDiary } from './diariesSlice'; import { setCanEdit, setActiveDiaryId, setCurrentlyEditing } from '../entry/editorSlice'; import { showAlert } from '../../util'; import { Link } from 'react-router-dom'; import { useAppDispatch } from '../../store'; interface Props { diary: Diary; } const buttonStyle: React.CSSProperties = { fontSize: '0.7em', margin: '0 0.5em', }; const DiaryTile: FC<Props> = (props) => { const [diary, setDiary] = useState(props.diary); const [isEditing, setIsEditing] = useState(false); const dispatch = useAppDispatch(); const totalEntries = props.diary?.entryIds?.length; const saveChanges = () => { http .put<Diary, Diary>(`/diaries/${diary.id}`, diary) .then((diary) => { if (diary) { dispatch(updateDiary(diary)); showAlert('Saved!', 'success'); } }) .finally(() => { setIsEditing(false); }); }; return ( <div className="diary-tile"> <h2 className="title" title="Click to edit" onClick={() => setIsEditing(true)} style= > {isEditing ? ( <input value={diary.title} onChange={(e) => { setDiary({ ...diary, title: e.target.value, }); }} onKeyUp={(e) => { if (e.key === 'Enter') { saveChanges(); } }} /> ) : ( <span>{diary.title}</span> )} </h2> <p className="subtitle">{totalEntries ?? '0'} saved entries</p> <div style=> <button style={buttonStyle} onClick={() => { dispatch(setCanEdit(true)); dispatch(setActiveDiaryId(diary.id as string)); dispatch(setCurrentlyEditing(null)); }} > Add New Entry </button> <Link to={`diary/${diary.id}`} style=> <button className="secondary" style={buttonStyle}> View all → </button> </Link> </div> </div> ); }; export default DiaryTile;
In this file, we receive a diary object as a prop and display the data in our component. Notice that we use local state and component props for our data display here. That’s because you don’t have to manage all your app’s state using RedUX. Sharing data using props, and maintaining local state in your components is acceptable and encouraged in some cases.
Next, let’s create a component that will display a list of a diary’s entries, with the last updated entries at the top of the list. Ensure you are in the src/features/diary directory, then create a file named DiaryEntriesList.tsx and add the following code to the file:
import React, { FC, useEffect } from 'react'; import { useParams, Link } from 'react-router-dom'; import { useSelector } from 'react-redUX'; import { RootState } from '../../rootReducer'; import http from '../../services/api'; import { Entry } from '../../interfaces/entry.interface'; import { setEntries } from '../entry/entriesSlice'; import { setCurrentlyEditing, setCanEdit } from '../entry/editorSlice'; import dayjs from 'dayjs'; import { useAppDispatch } from '../../store'; const DiaryEntriesList: FC = () => { const { entries } = useSelector((state: RootState) => state); const dispatch = useAppDispatch(); const { id } = useParams(); useEffect(() => { if (id != null) { http .get<null, { entries: Entry[] }>(`/diaries/entries/${id}`) .then(({ entries: _entries }) => { if (_entries) { const sortByLastUpdated = _entries.sort((a, b) => { return dayjs(b.updatedAt).unix() - dayjs(a.updatedAt).unix(); }); dispatch(setEntries(sortByLastUpdated)); } }); } }, [id, dispatch]); return ( <div className="entries"> <header> <Link to="/"> <h3>← Go Back</h3> </Link> </header> <ul> {entries.map((entry) => ( <li key={entry.id} onClick={() => { dispatch(setCurrentlyEditing(entry)); dispatch(setCanEdit(true)); }} > {entry.title} </li> ))} </ul> </div> ); }; export default DiaryEntriesList;
Here, we subscribe to the entries property of our app’s state, and have our effect fetch a diary’s entry only run when a property, id, changes. This property’s value is gotten from our URL as a path parameter using the useParams() hook from react-router. In the next step, we will create a component that will enable users to create and view diaries, as well as render a diary’s entries when it is in focus.
Create a file named Diaries.tsx while still in the same directory, and add the following code to the file:
import React, { FC, useEffect } from 'react'; import { useSelector } from 'react-redUX'; import { RootState } from '../../rootReducer'; import http from '../../services/api'; import { Diary } from '../../interfaces/diary.interface'; import { addDiary } from './diariesSlice'; import Swal from 'sweetalert2'; import { setUser } from '../auth/userSlice'; import DiaryTile from './DiaryTile'; import { User } from '../../interfaces/user.interface'; import { Route, Switch } from 'react-router-dom'; import DiaryEntriesList from './DiaryEntriesList'; import { useAppDispatch } from '../../store'; import dayjs from 'dayjs'; const Diaries: FC = () => { const dispatch = useAppDispatch(); const diaries = useSelector((state: RootState) => state.diaries); const user = useSelector((state: RootState) => state.user); useEffect(() => { const fetchDiaries = async () => { if (user) { http.get<null, Diary[]>(`diaries/${user.id}`).then((data) => { if (data && data.length > 0) { const sortedByUpdatedAt = data.sort((a, b) => { return dayjs(b.updatedAt).unix() - dayjs(a.updatedAt).unix(); }); dispatch(addDiary(sortedByUpdatedAt)); } }); } }; fetchDiaries(); }, [dispatch, user]); const createDiary = async () => { const result = await Swal.mixin({ input: 'text', confirmButtonText: 'Next →', showCancelButton: true, progressSteps: ['1', '2'], }).queue([ { titleText: 'Diary title', input: 'text', }, { titleText: 'Private or public diary?', input: 'radio', inputOptions: { private: 'Private', public: 'Public', }, inputValue: 'private', }, ]); if (result.value) { const { value } = result; const { diary, user: _user, } = await http.post<Partial<Diary>, { diary: Diary; user: User }>('/diaries/', { title: value[0], type: value[1], userId: user?.id, }); if (diary && user) { dispatch(addDiary([diary] as Diary[])); dispatch(addDiary([diary] as Diary[])); dispatch(setUser(_user)); return Swal.fire({ titleText: 'All done!', confirmButtonText: 'OK!', }); } } Swal.fire({ titleText: 'Cancelled', }); }; return ( <div style=> <Switch> <Route path="/diary/:id"> <DiaryEntriesList /> </Route> <Route path="/"> <button onClick={createDiary}>Create New</button> {diaries.map((diary, idx) => ( <DiaryTile key={idx} diary={diary} /> ))} </Route> </Switch> </div> ); }; export default Diaries;
In this component, we have a function to fetch the user’s diaries inside a useEffect hook, and a function to create a new diary. We also render our components in react-router’s <Route /> component, rendering a diary’s entries if its id matches the path param in the route /diary/:id, or otherwise rendering a list of the user’s diaries.
To wrap things up, let’s update the Home.tsx component. First, update the imports to look like the following:
import React, { FC } from 'react'; import Diaries from '../diary/Diaries'; import Editor from '../entry/Editor';
Then, change the component’s return statement to the following:
return ( <div className="two-cols"> <div className="left"> <Diaries /> </div> <div className="right"> <Editor /> </div> </div>
Finally, replace the contents of the index.css file in your app’s src directory with the following code:
:root { --primary-color: #778899; --error-color: #f85032; --text-color: #0d0d0d; --transition: all ease-in-out 0.3s; } body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } html, body, #root { height: 100%; } *, *:before, *:after { box-sizing: border-box; } .auth { display: flex; align-items: center; height: 100%; } .card { background: #fff; padding: 3rem; text-align: center; box-shadow: 2px 8px 12px rgba(0, 0, 0, 0.1); max-width: 450px; width: 90%; margin: 0 auto; } .inputWrapper { margin: 1rem auto; width: 100%; } input:not([type='checkbox']), button { border-radius: 0.5rem; width: 100%; } input:not([type='checkbox']), textarea { border: 2px solid rgba(0, 0, 0, 0.1); padding: 1em; color: var(--text-color); transition: var(--transition); } input:not([type='checkbox']):focus, textarea:focus { outline: none; border-color: var(--primary-color); } button { appearance: none; border: 1px solid var(--primary-color); color: #fff; background-color: var(--primary-color); text-transform: uppercase; font-weight: bold; outline: none; cursor: pointer; padding: 1em; box-shadow: 1px 4px 6px rgba(0, 0, 0, 0.1); transition: var(--transition); } button.secondary { color: var(--primary-color); background-color: #fff; border-color: #fff; } button:hover, button:focus { box-shadow: 1px 6px 8px rgba(0, 0, 0, 0.1); } .error { margin: 0; margin-top: 0.2em; font-size: 0.8em; color: var(--error-color); animation: 0.3s ease-in-out forwards fadeIn; } .two-cols { display: flex; flex-wrap: wrap; height: 100vh; } .two-cols .left { border-right: 1px solid rgba(0, 0, 0, 0.1); height: 100%; overflow-y: scroll; } .two-cols .right { overflow-y: auto; } .title { font-size: 1.3rem; } .subtitle { font-size: 0.9rem; opacity: 0.85; } .title, .subtitle { margin: 0; } .diary-tile { border-bottom: 1px solid rgba(0, 0, 0, 0.1); padding: 1em; } .editor { height: 100%; padding: 1em; } .editor input { width: 100%; } .editor textarea { width: 100%; height: calc(100vh - 160px); } .entries ul { list-style: none; padding: 0; } .entries li { border-top: 1px solid rgba(0, 0, 0, 0.1); padding: 0.5em; cursor: pointer; } .entries li:nth-child(even) { background: rgba(0, 0, 0, 0.1); } @media (min-width: 768px) { .two-cols .left { width: 25%; } .two-cols .right { width: 75%; } } @keyframes fadeIn { 0% { opacity: 0; } 100% { opacity: 0.8; } }
That’s it! You can now run npm start or yarn start and check out the final app at http://localhost:3000.
Final App Home Screen (Unauthenticated User). (Large preview)
Conclusion
In this guide, you have learned how to rapidly develop applications using RedUX. You also learned about good practices to follow when working with RedUX and React, in order to make debugging and extending your applications easier. This guide is by no means extensive as there are still ongoing discussions surrounding Redux and some of its concepts. Please check out the Redux and React-Redux docs if you’d like to learn more about using RedUX in your React projects.
References
(yk)
Website Design & SEO Delray Beach by DBL07.co
Delray Beach SEO
source http://www.scpie.org/setting-up-redux-for-use-in-a-real-world-application/ source https://scpie1.blogspot.com/2020/08/setting-up-redux-for-use-in-real-world.html
0 notes
riichardwilson · 5 years ago
Text
Setting Up Redux For Use In A Real-World Application
About The Author
I love building software for the web, writing about web technologies, and playing video games. More about Jerry …
RedUX is a robust state-management library for single-page Javascript apps. It is described on the official documentation as a predictable state container for Javascript applications and it’s fairly simple to learn the concepts and implement RedUX in a simple app. Going from a simple counter app to a real-world app, however, can be quite the jump.
RedUX is an important library in the React ecosystem, and almost the default to use when working on React applications that involve state management. As such, the importance of knowing how it works cannot be overestimated.
This guide will walk the reader through setting up RedUX in a fairly complex React application and introduce the reader to “best practices” configuration along the way. It will be beneficial to beginners especially, and anyone who wants to fill in the gaps in their knowledge of RedUX.
Introducing Redux
RedUX is a library that aims to solve the problem of state management in JavaScript apps by imposing restrictions on how and when state updates can happen. These restrictions are formed from RedUX’s “three principles” which are:
Single source of truth All of your application’s state is held in a RedUX store. This state can be represented visually as a tree with a single ancestor, and the store provides methods for reading the current state and subscribing to changes from anywhere within your app.
State is read-only The only way to change the state is to send the data as a plain object, called an action. You can think about actions as a way of saying to the state, “I have some data I would like to insert/update/delete”.
Changes are made with pure functions To change your app’s state, you write a function that takes the previous state and an action and returns a new state object as the next state. This function is called a reducer, and it is a pure function because it returns the same output for a given set of inputs.
The last principle is the most important in RedUX, and this is where the magic of RedUX happens. Reducer functions must not contain unpredictable code, or perform side-effects such as network requests, and should not directly mutate the state object.
RedUX is a great tool, as we’ll learn later in this guide, but it doesn’t come without its challenges or tradeoffs. To help make the process of writing RedUX efficient and more enjoyable, the RedUX team offers a toolkit that abstracts over the process of setting up a RedUX store and provides helpful RedUX add-ons and utilities that help to simplify application code. For example, the library uses Immer.js, a library that makes it possible for you to write “mutative” immutable update logic, under the hood.
Recommended reading: Better Reducers With Immer
In this guide, we will explore RedUX by building an application that lets authenticated users create and manage digital diaries.
Building Diaries.app
As stated in the previous section, we will be taking a closer look at RedUX by building an app that lets users create and manage diaries. We will be building our application using React, and we’ll set up Mirage as our API mocking server since we won’t have access to a real server in this guide.
Starting a Project and Installing Dependencies
Let’s get started on our project. First, bootstrap a new React application using create-react-app:
Using npx:
npx create-react-app diaries-app --template typescript
We are starting with the TypeScript template, as we can improve our development experience by writing type-safe code.
Now, let’s install the dependencies we’ll be needing. Navigate into your newly created project directory
cd diaries-app
And run the following commands:
npm install --save redUX react-redUX @redUXjs/toolkit
npm install --save axios react-router-dom react-hook-form yup dayjs markdown-to-jsx sweetalert2
npm install --save-dev miragejs @types/react-redUX @types/react-router-dom @types/yup @types/markdown-to-jsx
The first command will install RedUX, React-RedUX (official React bindings for RedUX), and the RedUX toolkit.
The second command installs some extra packages which will be useful for the app we’ll be building but are not required to work with RedUX.
The last command installs Mirage and type declarations for the packages we installed as devDependencies.
Describing the Application’s Initial State
Let’s go over our application’s requirements in detail. The application will allow authenticated users to create or modify existing diaries. Diaries are private by default, but they can be made public. Finally, diary entries will be sorted by their last modified date.
This relationship should look something like this:
An Overview of the Application’s Data Model. (Large preview)
Armed with this information, we can now model our application’s state. First, we will create an interface for each of the following resources: User, Diary and DiaryEntry. Interfaces in Typescript describe the shape of an object.
Go ahead and create a new directory named interfaces in your app’s src sub-directory:
cd src && mkdir interfaces
Next, run the following commands in the directory you just created:
touch entry.interface.ts touch diary.interface.ts touch user.interface.ts
This will create three files named entry.interface.ts, diary.interface.ts and user.interface.ts respectively. I prefer to keep interfaces that would be used in multiple places across my app in a single location.
Open entry.interface.ts and add the following code to set up the Entry interface:
export interface Entry { id?: string; title: string; content: string; createdAt?: string; updatedAt?: string; diaryId?: string; }
A typical diary entry will have a title and some content, as well as information about when it was created or last updated. We’ll get back to the diaryId property later.
Next, add the following to diary.interface.ts:
export interface Diary { id?: string; title: string; type: 'private' | 'public'; createdAt?: string; updatedAt?: string; userId?: string; entryIds: string[] | null; }
Here, we have a type property which expects an exact value of either ‘private’ or ‘public’, as diaries must be either private or public. Any other value will throw an error in the TypeScript compiler.
We can now describe our User object in the user.interface.ts file as follows:
export interface User { id?: string; username: string; email: string; password?: string; diaryIds: string[] | null; }
With our type definitions finished and ready to be used across our app, let’s setup our mock API server using Mirage.
Setting up API Mocking with MirageJS
Since this tutorial is focused on RedUX, we will not go into the details of setting up and using Mirage in this section. Please check out this excellent series if you would like to learn more about Mirage.
To get started, navigate to your src directory and create a file named server.ts by running the following commands:
mkdir -p services/mirage cd services/mirage # ~/diaries-app/src/services/mirage touch server.ts
Next, open the server.ts file and add the following code:
import { Server, Model, Factory, belongsTo, hasMany, Response } from 'miragejs'; export const handleErrors = (error: any, message = 'An error ocurred') => { return new Response(400, undefined, { data: { message, isError: true, }, }); }; export const setupServer = (env?: string): Server => { return new Server({ environment: env ?? 'development', models: { entry: Model.extend({ diary: belongsTo(), }), diary: Model.extend({ entry: hasMany(), user: belongsTo(), }), user: Model.extend({ diary: hasMany(), }), }, factories: { user: Factory.extend({ username: 'test', password: 'password', email: '[email protected]', }), }, seeds: (server): any => { server.create('user'); }, routes(): void { this.urlPrefix = 'https://diaries.app'; }, }); };
In this file, we are exporting two functions. A utility function for handling errors, and setupServer(), which returns a new server instance. The setupServer() function takes an optional argument which can be used to change the server’s environment. You can use this to set up Mirage for testing later.
We have also defined three models in the server’s models property: User, Diary and Entry. Remember that earlier we set up the Entry interface with a property named diaryId. This value will be automatically set to the id the entry is being saved to. Mirage uses this property to establish a relationship between an Entry and a Diary. The same thing also happens when a user creates a new diary: userId is automatically set to that user’s id.
We seeded the database with a default user and configured Mirage to intercept all requests from our app starting with https://diaries.app. Notice that we haven’t configured any route handlers yet. Let’s go ahead and create a few.
Ensure that you are in the src/services/mirage directory, then create a new directory named routes using the following command:
# ~/diaries-app/src/services/mirage mkdir routes
cd to the newly created directory and create a file named user.ts:
cd routes touch user.ts
Next, paste the following code in the user.ts file:
import { Response, Request } from 'miragejs'; import { handleErrors } from '../server'; import { User } from '../../../interfaces/user.interface'; import { randomBytes } from 'crypto'; const generateToken = () => randomBytes(8).toString('hex'); export interface AuthResponse { token: string; user: User; } const login = (schema: any, req: Request): AuthResponse | Response => { const { username, password } = JSON.parse(req.requestBody); const user = schema.users.findBy({ username }); if (!user) { return handleErrors(null, 'No user with that username exists'); } if (password !== user.password) { return handleErrors(null, 'Password is incorrect'); } const token = generateToken(); return { user: user.attrs as User, token, }; }; const signup = (schema: any, req: Request): AuthResponse | Response => { const data = JSON.parse(req.requestBody); const exUser = schema.users.findBy({ username: data.username }); if (exUser) { return handleErrors(null, 'A user with that username already exists.'); } const user = schema.users.create(data); const token = generateToken(); return { user: user.attrs as User, token, }; }; export default { login, signup, };
The login and signup methods here receive a Schema class and a fake Request object and, upon validating the password or checking that the login does not already exist, return the existing user or a new user respectively. We use the Schema object to interact with Mirage’s ORM, while the Request object contains information about the intercepted request including the request body and headers.
Next, let’s add methods for working with diaries and diary entries. Create a file named diary.ts in your routes directory:
touch diary.ts
Update the file with the following methods for working with Diary resources:
export const create = ( schema: any, req: Request ): { user: User; diary: Diary } | Response => { try { const { title, type, userId } = JSON.parse(req.requestBody) as Partial< Diary >; const exUser = schema.users.findBy({ id: userId }); if (!exUser) { return handleErrors(null, 'No such user exists.'); } const now = dayjs().format(); const diary = exUser.createDiary({ title, type, createdAt: now, updatedAt: now, }); return { user: { ...exUser.attrs, }, diary: diary.attrs, }; } catch (error) { return handleErrors(error, 'Failed to create Diary.'); } }; export const updateDiary = (schema: any, req: Request): Diary | Response => { try { const diary = schema.diaries.find(req.params.id); const data = JSON.parse(req.requestBody) as Partial<Diary>; const now = dayjs().format(); diary.update({ ...data, updatedAt: now, }); return diary.attrs as Diary; } catch (error) { return handleErrors(error, 'Failed to update Diary.'); } }; export const getDiaries = (schema: any, req: Request): Diary[] | Response => { try { const user = schema.users.find(req.params.id); return user.diary as Diary[]; } catch (error) { return handleErrors(error, 'Could not get user diaries.'); } };
Next, let’s add some methods for working with diary entries:
export const addEntry = ( schema: any, req: Request ): { diary: Diary; entry: Entry } | Response => { try { const diary = schema.diaries.find(req.params.id); const { title, content } = JSON.parse(req.requestBody) as Partial<Entry>; const now = dayjs().format(); const entry = diary.createEntry({ title, content, createdAt: now, updatedAt: now, }); diary.update({ ...diary.attrs, updatedAt: now, }); return { diary: diary.attrs, entry: entry.attrs, }; } catch (error) { return handleErrors(error, 'Failed to save entry.'); } }; export const getEntries = ( schema: any, req: Request ): { entries: Entry[] } | Response => { try { const diary = schema.diaries.find(req.params.id); return diary.entry; } catch (error) { return handleErrors(error, 'Failed to get Diary entries.'); } }; export const updateEntry = (schema: any, req: Request): Entry | Response => { try { const entry = schema.entries.find(req.params.id); const data = JSON.parse(req.requestBody) as Partial<Entry>; const now = dayjs().format(); entry.update({ ...data, updatedAt: now, }); return entry.attrs as Entry; } catch (error) { return handleErrors(error, 'Failed to update entry.'); } };
Finally, let’s add the necessary imports at the top of the file:
import { Response, Request } from 'miragejs'; import { handleErrors } from '../server'; import { Diary } from '../../../interfaces/diary.interface'; import { Entry } from '../../../interfaces/entry.interface'; import dayjs from 'dayjs'; import { User } from '../../../interfaces/user.interface';
In this file, we have exported methods for working with the Diary and Entry models. In the create method, we call a method named user.createDiary() to save a new diary and associate it to a user account.
The addEntry and updateEntry methods create and correctly associate a new entry to a diary or update an existing entry’s data respectively. The latter also updates the entry’s updatedAt property with the current timestamp. The updateDiary method also updates a diary with the timestamp the change was made. Later, we’ll be sorting the records we receive from our network request with this property.
We also have a getDiaries method which retrieves a user’s diaries and a getEntries methods which retrieves a selected diary’s entries.
We can now update our server to use the methods we just created. Open server.ts to include the files:
import { Server, Model, Factory, belongsTo, hasMany, Response } from 'miragejs'; import user from './routes/user'; import * as diary from './routes/diary';
Then, update the server’s route property with the routes we want to handle:
export const setupServer = (env?: string): Server => { return new Server({ // ... routes(): void { this.urlPrefix = 'https://diaries.app'; this.get('/diaries/entries/:id', diary.getEntries); this.get('/diaries/:id', diary.getDiaries); this.post('/auth/login', user.login); this.post('/auth/signup', user.signup); this.post('/diaries/', diary.create); this.post('/diaries/entry/:id', diary.addEntry); this.put('/diaries/entry/:id', diary.updateEntry); this.put('/diaries/:id', diary.updateDiary); }, }); };
With this change, when a network request from our app matches one of the route handlers, Mirage intercepts the request and invokes the respective route handler functions.
Next, we’ll proceed to make our application aware of the server. Open src/index.tsx and import the setupServer() method:
import { setupServer } from './services/mirage/server';
And add the following code before ReactDOM.render():
if (process.env.NODE_ENV === 'development') { setupServer(); }
The check in the code block above ensures that our Mirage server will run only while we are in development mode.
One last thing we need to do before moving on to the RedUX bits is configure a custom Axios instance for use in our app. This will help to reduce the amount of code we’ll have to write later on.
Create a file named api.ts under src/services and add the following code to it:
import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios'; import { showAlert } from '../util'; const http: AxiosInstance = axios.create({ baseURL: 'https://diaries.app', }); http.defaults.headers.post['Content-Type'] = 'application/json'; http.interceptors.response.use( async (response: AxiosResponse): Promise => { if (response.status >= 200 && response.status < 300) { return response.data; } }, (error: AxiosError) => { const { response, request }: { response?: AxiosResponse; request?: XMLHttpRequest; } = error; if (response) { if (response.status >= 400 && response.status < 500) { showAlert(response.data?.data?.message, 'error'); return null; } } else if (request) { showAlert('Request failed. Please try again.', 'error'); return null; } return Promise.reject(error); } ); export default http;
In this file, we are exporting an Axios instance modified to include our app’s API url, https://diaries.app. We have configured an interceptor to handle success and error responses, and we display error messages using a sweetalert toast which we will configure in the next step.
Create a file named util.ts in your src directory and paste the following code in it:
import Swal, { SweetAlertIcon } from 'sweetalert2'; export const showAlert = (titleText = 'Something happened.', alertType?: SweetAlertIcon): void => { Swal.fire({ titleText, position: 'top-end', timer: 3000, timerProgressBar: true, toast: true, showConfirmButton: false, showCancelButton: true, cancelButtonText: 'Dismiss', icon: alertType, showClass: { popup: 'swal2-noanimation', backdrop: 'swal2-noanimation', }, hideClass: { popup: '', backdrop: '', }, }); };
This file exports a function that displays a toast whenever it is invoked. The function accepts parameters to allow you set the toast message and type. For example, we are showing an error toast in the Axios response error interceptor like this:
showAlert(response.data?.data?.message, 'error');
Now when we make requests from our app while in development mode, they will be intercepted and handled by Mirage instead. In the next section, we will set up our RedUX store using RedUX toolkit.
Setting up a Redux Store
In this section, we are going to set up our store using the following exports from RedUX toolkit: configureStore(), getDefaultMiddleware() and createSlice(). Before we start, we should take a detailed look at what these exports do.
configureStore() is an abstraction over the RedUX createStore() function that helps simplify your code. It uses createStore() internally to set up your store with some useful development tools:
export const store = configureStore({ reducer: rootReducer, // a single reducer function or an object of slice reducers });
The createSlice() function helps simplify the process of creating action creators and slice reducers. It accepts an initial state, an object full of reducer functions, and a “slice name”, and automatically generates action creators and action types corresponding to the reducers and your state. It also returns a single reducer function, which can be passed to RedUX’s combineReducers() function as a “slice reducer”.
Remember that the state is a single tree, and a single root reducer manages changes to that tree. For maintainability, it is recommended to split your root reducer into “slices,” and have a “slice reducer” provide an initial value and calculate the updates to a corresponding slice of the state. These slices can be joined into a single reducer function by using combineReducers().
There are additional options for configuring the store. For example, you can pass an array of your own middleware to configureStore() or start up your app from a saved state using the preloadedState option. When you supply the middleware option, you have to define all the middleware you want added to the store. If you would like to retain the defaults when setting up your store, you can use getDefaultMiddleware() to get the default list of middleware:
export const store = configureStore({ // ... middleware: [...getDefaultMiddleware(), customMiddleware], });
Let’s now proceed to set up our store. We will adopt a “ducks-style” approach to structuring our files, specifically following the guidelines in practice from the Github Issues sample app. We will be organizing our code such that related components, as well as actions and reducers, live in the same directory. The final state object will look like this:
type RootState = { auth: { token: string | null; isAuthenticated: boolean; }; diaries: Diary[]; entries: Entry[]; user: User | null; editor: { canEdit: boolean; currentlyEditing: Entry | null; activeDiaryId: string | null; }; }
To get started, create a new directory named features under your src directory:
# ~/diaries-app/src mkdir features
Then, cd into features and create directories named auth, diary and entry:
cd features mkdir auth diary entry
cd into the auth directory and create a file named authSlice.ts:
cd auth # ~/diaries-app/src/features/auth touch authSlice.ts
Open the file and paste the following in it:
import { createSlice, PayloadAction } from '@redUXjs/toolkit'; interface AuthState { token: string | null; isAuthenticated: boolean; } const initialState: AuthState = { token: null, isAuthenticated: false, }; const auth = createSlice({ name: 'auth', initialState, reducers: { saveToken(state, { payload }: PayloadAction) { if (payload) { state.token = payload; } }, clearToken(state) { state.token = null; }, setAuthState(state, { payload }: PayloadAction) { state.isAuthenticated = payload; }, }, }); export const { saveToken, clearToken, setAuthState } = auth.actions; export default auth.reducer;
In this file, we’re creating a slice for the auth property of our app’s state using the createSlice() function introduced earlier. The reducers property holds a map of reducer functions for updating values in the auth slice. The returned object contains automatically generated action creators and a single slice reducer. We would need to use these in other files so, following the “ducks pattern”, we do named exports of the action creators, and a default export of the reducer function.
Let’s set up the remaining reducer slices according to the app state we saw earlier. First, create a file named userSlice.ts in the auth directory and add the following code to it:
import { createSlice, PayloadAction } from '@redUXjs/toolkit'; import { User } from '../../interfaces/user.interface'; const user = createSlice({ name: 'user', initialState: null as User | null, reducers: { setUser(state, { payload }: PayloadAction<User | null>) { return state = (payload != null) ? payload : null; }, }, }); export const { setUser } = user.actions; export default user.reducer;
This creates a slice reducer for the user property in our the application’s store. The setUser reducer function accepts a payload containing user data and updates the state with it. When no data is passed, we set the state’s user property to null.
Next, create a file named diariesSlice.ts under src/features/diary:
# ~/diaries-app/src/features cd diary touch diariesSlice.ts
Add the following code to the file:
import { createSlice, PayloadAction } from '@redUXjs/toolkit'; import { Diary } from '../../interfaces/diary.interface'; const diaries = createSlice({ name: 'diaries', initialState: [] as Diary[], reducers: { addDiary(state, { payload }: PayloadAction<Diary[]>) { const diariesToSave = payload.filter((diary) => { return state.findIndex((item) => item.id === diary.id) === -1; }); state.push(...diariesToSave); }, updateDiary(state, { payload }: PayloadAction<Diary>) { const { id } = payload; const diaryIndex = state.findIndex((diary) => diary.id === id); if (diaryIndex !== -1) { state.splice(diaryIndex, 1, payload); } }, }, }); export const { addDiary, updateDiary } = diaries.actions; export default diaries.reducer;
The “diaries” property of our state is an array containing the user’s diaries, so our reducer functions here all work on the state object they receive using array methods. Notice here that we are writing normal “mutative” code when working on the state. This is possible because the reducer functions we create using the createSlice() method are wrapped with Immer’s produce() method. This results in Immer returning a correct immutably updated result for our state regardless of us writing mutative code.
Next, create a file named entriesSlice.ts under src/features/entry:
# ~/diaries-app/src/features mkdir entry cd entry touch entriesSlice.ts
Open the file and add the following code:
import { createSlice, PayloadAction } from '@redUXjs/toolkit'; import { Entry } from '../../interfaces/entry.interface'; const entries = createSlice({ name: 'entries', initialState: [] as Entry[], reducers: { setEntries(state, { payload }: PayloadAction<Entry[] | null>) { return (state = payload != null ? payload : []); }, updateEntry(state, { payload }: PayloadAction<Entry>) { const { id } = payload; const index = state.findIndex((e) => e.id === id); if (index !== -1) { state.splice(index, 1, payload); } }, }, }); export const { setEntries, updateEntry } = entries.actions; export default entries.reducer;
The reducer functions here have logic similar to the previous slice’s reducer functions. The entries property is also an array, but it only holds entries for a single diary. In our app, this will be the diary currently in the user’s focus.
Finally, create a file named editorSlice.ts in src/features/entry and add the following to it:
import { createSlice, PayloadAction } from '@redUXjs/toolkit'; import { Entry } from '../../interfaces/entry.interface'; interface EditorState { canEdit: boolean; currentlyEditing: Entry | null; activeDiaryId: string | null; } const initialState: EditorState = { canEdit: false, currentlyEditing: null, activeDiaryId: null, }; const editor = createSlice({ name: 'editor', initialState, reducers: { setCanEdit(state, { payload }: PayloadAction<boolean>) { state.canEdit = payload != null ? payload : !state.canEdit; }, setCurrentlyEditing(state, { payload }: PayloadAction<Entry | null>) { state.currentlyEditing = payload; }, setActiveDiaryId(state, { payload }: PayloadAction<string>) { state.activeDiaryId = payload; }, }, }); export const { setCanEdit, setCurrentlyEditing, setActiveDiaryId } = editor.actions; export default editor.reducer;
Here, we have a slice for the editor property in state. We’ll be using the properties in this object to check if the user wants to switch to editing mode, which diary the edited entry belongs to, and what entry is going to be edited.
To put it all together, create a file named rootReducer.ts in the src directory with the following content:
import { combineReducers } from '@redUXjs/toolkit'; import authReducer from './features/auth/authSlice'; import userReducer from './features/auth/userSlice'; import diariesReducer from './features/diary/diariesSlice'; import entriesReducer from './features/entry/entriesSlice'; import editorReducer from './features/entry/editorSlice'; const rootReducer = combineReducers({ auth: authReducer, diaries: diariesReducer, entries: entriesReducer, user: userReducer, editor: editorReducer, }); export type RootState = ReturnType<typeof rootReducer>; export default rootReducer;
In this file, we’ve combined our slice reducers into a single root reducer with the combineReducers() function. We’ve also exported the RootState type, which will be useful later when we’re selecting values from the store. We can now use the root reducer (the default export of this file) to set up our store.
Create a file named store.ts with the following contents:
import { configureStore } from '@redUXjs/toolkit'; import rootReducer from './rootReducer'; import { useDispatch } from 'react-redUX'; const store = configureStore({ reducer: rootReducer, }); type AppDispatch = typeof store.dispatch; export const useAppDispatch = () => useDispatch<AppDispatch>(); export default store;
With this, we’ve created a store using the configureStore() export from RedUX toolkit. We’ve also exported an hook called useAppDispatch() which merely returns a typed useDispatch() hook.
Next, update the imports in index.tsx to look like the following:
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './app/App'; import * as serviceWorker from './serviceWorker'; import { setupServer } from './services/mirage/server'; import { Provider } from 'react-redUX'; import store from './store'; // ...
Finally, make the store available to the app’s components by wrapping <App /> (the top-level component) with <Provider />:
ReactDOM.render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode>, document.getElementById('root') );
Now, if you start your app and you navigate to http://localhost:3000 with the Redux Dev Tools extension enabled, you should see the following in your app’s state:
Initial State in RedUX Dev Tools Extension. (Large preview)
Great work so far, but we’re not quite finished yet. In the next section, we will design the app’s User Interface and add functionality using the store we’ve just created.
Designing The Application User Interface
To see RedUX in action, we are going to build a demo app. In this section, we will connect our components to the store we’ve created and learn to dispatch actions and modify the state using reducer functions. We will also learn how to read values from the store. Here’s what our RedUX-powered application will look like.
Home page showing an authenticated user’s diaries. (Large preview)
Screenshots of final app. (Large preview)
Setting up the Authentication Feature
To get started, move App.tsx and its related files from the src directory to its own directory like this:
# ~/diaries-app/src mkdir app mv App.tsx App.test.tsx app
You can delete the App.css and logo.svg files as we won’t be needing them.
Next, open the App.tsx file and replace its contents with the following:
import React, { FC, lazy, Suspense } from 'react'; import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; import { useSelector } from 'react-redUX'; import { RootState } from '../rootReducer'; const Auth = lazy(() => import('../features/auth/Auth')); const Home = lazy(() => import('../features/home/Home')); const App: FC = () => { const isLoggedIn = useSelector( (state: RootState) => state.auth.isAuthenticated ); return ( <Router> <Switch> <Route path="/"> <Suspense fallback={<p>Loading...</p>}> {isLoggedIn ? <Home /> : <Auth />} </Suspense> </Route> </Switch> </Router> ); }; export default App;
Here we have set up our app to render an <Auth /> component if the user is unauthenticated, or otherwise render a <Home /> component. We haven’t created either of these components yet, so let’s fix that. Create a file named Auth.tsx under src/features/auth and add the following contents to the file:
import React, { FC, useState } from 'react'; import { useForm } from 'react-hook-form'; import { User } from '../../interfaces/user.interface'; import * as Yup from 'yup'; import http from '../../services/api'; import { saveToken, setAuthState } from './authSlice'; import { setUser } from './userSlice'; import { AuthResponse } from '../../services/mirage/routes/user'; import { useAppDispatch } from '../../store'; const schema = Yup.object().shape({ username: Yup.string() .required('What? No username?') .max(16, 'Username cannot be longer than 16 characters'), password: Yup.string().required('Without a password, "None shall pass!"'), email: Yup.string().email('Please provide a valid email address ([email protected])'), }); const Auth: FC = () => { const { handleSubmit, register, errors } = useForm<User>({ validationSchema: schema, }); const [isLogin, setIsLogin] = useState(true); const [loading, setLoading] = useState(false); const dispatch = useAppDispatch(); const submitForm = (data: User) => { const path = isLogin ? '/auth/login' : '/auth/signup'; http .post<User, AuthResponse>(path, data) .then((res) => { if (res) { const { user, token } = res; dispatch(saveToken(token)); dispatch(setUser(user)); dispatch(setAuthState(true)); } }) .catch((error) => { console.log(error); }) .finally(() => { setLoading(false); }); }; return ( <div className="auth"> <div className="card"> <form onSubmit={handleSubmit(submitForm)}> <div className="inputWrapper"> <input ref={register} name="username" placeholder="Username" /> {errors && errors.username && ( <p className="error">{errors.username.message}</p> )} </div> <div className="inputWrapper"> <input ref={register} name="password" type="password" placeholder="Password" /> {errors && errors.password && ( <p className="error">{errors.password.message}</p> )} </div> {!isLogin && ( <div className="inputWrapper"> <input ref={register} name="email" placeholder="Email (optional)" /> {errors && errors.email && ( <p className="error">{errors.email.message}</p> )} </div> )} <div className="inputWrapper"> <button type="submit" disabled={loading}> {isLogin ? 'Login' : 'Create account'} </button> </div> <p onClick={() => setIsLogin(!isLogin)} style= > {isLogin ? 'No account? Create one' : 'Already have an account?'} </p> </form> </div> </div> ); }; export default Auth;
In this component, we have set up a form for users to log in, or to create an account. Our form fields are validated using Yup and, on successfully authenticating a user, we use our useAppDispatch hook to dispatch the relevant actions. You can see the dispatched actions and the changes made to your state in the RedUX DevTools Extension:
Dispatched Actions with Changes Tracked in RedUX Dev Tools Extensions. (Large preview)
Finally, create a file named Home.tsx under src/features/home and add the following code to the file:
import React, { FC } from 'react'; const Home: FC = () => { return ( <div> <p>Welcome user!</p> </div> ); }; export default Home;
For now, we are just displaying some text to the authenticated user. As we build the rest of our application, we will be updating this file.
Setting up the Editor
The next component we are going to build is the editor. Though basic, we will enable support for rendering markdown content using the markdown-to-jsx library we installed earlier.
First, create a file named Editor.tsx in the src/features/entry directory. Then, add the following code to the file:
import React, { FC, useState, useEffect } from 'react'; import { useSelector } from 'react-redUX'; import { RootState } from '../../rootReducer'; import Markdown from 'markdown-to-jsx'; import http from '../../services/api'; import { Entry } from '../../interfaces/entry.interface'; import { Diary } from '../../interfaces/diary.interface'; import { setCurrentlyEditing, setCanEdit } from './editorSlice'; import { updateDiary } from '../diary/diariesSlice'; import { updateEntry } from './entriesSlice'; import { showAlert } from '../../util'; import { useAppDispatch } from '../../store'; const Editor: FC = () => { const { currentlyEditing: entry, canEdit, activeDiaryId } = useSelector( (state: RootState) => state.editor ); const [editedEntry, updateEditedEntry] = useState(entry); const dispatch = useAppDispatch(); const saveEntry = async () => { if (activeDiaryId == null) { return showAlert('Please select a diary.', 'warning'); } if (entry == null) { http .post<Entry, { diary: Diary; entry: Entry }>( `/diaries/entry/${activeDiaryId}`, editedEntry ) .then((data) => { if (data != null) { const { diary, entry: _entry } = data; dispatch(setCurrentlyEditing(_entry)); dispatch(updateDiary(diary)); } }); } else { http .put<Entry, Entry>(`diaries/entry/${entry.id}`, editedEntry) .then((_entry) => { if (_entry != null) { dispatch(setCurrentlyEditing(_entry)); dispatch(updateEntry(_entry)); } }); } dispatch(setCanEdit(false)); }; useEffect(() => { updateEditedEntry(entry); }, [entry]); return ( <div className="editor"> <header style= > {entry && !canEdit ? ( <h4> {entry.title} <a href="#edit" onClick={(e) => { e.preventDefault(); if (entry != null) { dispatch(setCanEdit(true)); } }} style= > (Edit) </a> </h4> ) : ( <input value={editedEntry?.title ?? ''} disabled={!canEdit} onChange={(e) => { if (editedEntry) { updateEditedEntry({ ...editedEntry, title: e.target.value, }); } else { updateEditedEntry({ title: e.target.value, content: '', }); } }} /> )} </header> {entry && !canEdit ? ( <Markdown>{entry.content}</Markdown> ) : ( <> <textarea disabled={!canEdit} placeholder="Supports markdown!" value={editedEntry?.content ?? ''} onChange={(e) => { if (editedEntry) { updateEditedEntry({ ...editedEntry, content: e.target.value, }); } else { updateEditedEntry({ title: '', content: e.target.value, }); } }} /> <button onClick={saveEntry} disabled={!canEdit}> Save </button> </> )} </div> ); }; export default Editor;
Let’s break down what’s happening in the Editor component.
First, we are picking some values (with correctly inferred types) from the app’s state using the useSelector() hook from react-redUX. In the next line, we have a stateful value called editedEntry whose initial value is set to the editor.currentlyEditing property we’ve selected from the store.
Next, we have the saveEntry function which updates or creates a new entry in the API, and dispatches the respective RedUX action.
Finally, we have a useEffect that is fired when the editor.currentlyEditing property changes. Our editor’s UI (in the component’s return function) has been set up to respond to changes in the state. For example, rendering the entry’s content as JSX elements when the user isn’t editing.
With that, the app’s Entry feature should be completely set up. In the next section, we will finish building the Diary feature and then import the main components in the Home component we created earlier.
Final Steps
To finish up our app, we will first create components for the Diary feature. Then, we will update the Home component with the primary exports from the Diary and Entry features. Finally, we will add some styling to give our app the required pizzazz!
Let’s start by creating a file in src/features/diary named DiaryTile.tsx. This component will present information about a diary and its entries, and allow the user to edit the diary’s title. Add the following code to the file:
import React, { FC, useState } from 'react'; import { Diary } from '../../interfaces/diary.interface'; import http from '../../services/api'; import { updateDiary } from './diariesSlice'; import { setCanEdit, setActiveDiaryId, setCurrentlyEditing } from '../entry/editorSlice'; import { showAlert } from '../../util'; import { Link } from 'react-router-dom'; import { useAppDispatch } from '../../store'; interface Props { diary: Diary; } const buttonStyle: React.CSSProperties = { fontSize: '0.7em', margin: '0 0.5em', }; const DiaryTile: FC<Props> = (props) => { const [diary, setDiary] = useState(props.diary); const [isEditing, setIsEditing] = useState(false); const dispatch = useAppDispatch(); const totalEntries = props.diary?.entryIds?.length; const saveChanges = () => { http .put<Diary, Diary>(`/diaries/${diary.id}`, diary) .then((diary) => { if (diary) { dispatch(updateDiary(diary)); showAlert('Saved!', 'success'); } }) .finally(() => { setIsEditing(false); }); }; return ( <div className="diary-tile"> <h2 className="title" title="Click to edit" onClick={() => setIsEditing(true)} style= > {isEditing ? ( <input value={diary.title} onChange={(e) => { setDiary({ ...diary, title: e.target.value, }); }} onKeyUp={(e) => { if (e.key === 'Enter') { saveChanges(); } }} /> ) : ( <span>{diary.title}</span> )} </h2> <p className="subtitle">{totalEntries ?? '0'} saved entries</p> <div style=> <button style={buttonStyle} onClick={() => { dispatch(setCanEdit(true)); dispatch(setActiveDiaryId(diary.id as string)); dispatch(setCurrentlyEditing(null)); }} > Add New Entry </button> <Link to={`diary/${diary.id}`} style=> <button className="secondary" style={buttonStyle}> View all → </button> </Link> </div> </div> ); }; export default DiaryTile;
In this file, we receive a diary object as a prop and display the data in our component. Notice that we use local state and component props for our data display here. That’s because you don’t have to manage all your app’s state using RedUX. Sharing data using props, and maintaining local state in your components is acceptable and encouraged in some cases.
Next, let’s create a component that will display a list of a diary’s entries, with the last updated entries at the top of the list. Ensure you are in the src/features/diary directory, then create a file named DiaryEntriesList.tsx and add the following code to the file:
import React, { FC, useEffect } from 'react'; import { useParams, Link } from 'react-router-dom'; import { useSelector } from 'react-redUX'; import { RootState } from '../../rootReducer'; import http from '../../services/api'; import { Entry } from '../../interfaces/entry.interface'; import { setEntries } from '../entry/entriesSlice'; import { setCurrentlyEditing, setCanEdit } from '../entry/editorSlice'; import dayjs from 'dayjs'; import { useAppDispatch } from '../../store'; const DiaryEntriesList: FC = () => { const { entries } = useSelector((state: RootState) => state); const dispatch = useAppDispatch(); const { id } = useParams(); useEffect(() => { if (id != null) { http .get<null, { entries: Entry[] }>(`/diaries/entries/${id}`) .then(({ entries: _entries }) => { if (_entries) { const sortByLastUpdated = _entries.sort((a, b) => { return dayjs(b.updatedAt).unix() - dayjs(a.updatedAt).unix(); }); dispatch(setEntries(sortByLastUpdated)); } }); } }, [id, dispatch]); return ( <div className="entries"> <header> <Link to="/"> <h3>← Go Back</h3> </Link> </header> <ul> {entries.map((entry) => ( <li key={entry.id} onClick={() => { dispatch(setCurrentlyEditing(entry)); dispatch(setCanEdit(true)); }} > {entry.title} </li> ))} </ul> </div> ); }; export default DiaryEntriesList;
Here, we subscribe to the entries property of our app’s state, and have our effect fetch a diary’s entry only run when a property, id, changes. This property’s value is gotten from our URL as a path parameter using the useParams() hook from react-router. In the next step, we will create a component that will enable users to create and view diaries, as well as render a diary’s entries when it is in focus.
Create a file named Diaries.tsx while still in the same directory, and add the following code to the file:
import React, { FC, useEffect } from 'react'; import { useSelector } from 'react-redUX'; import { RootState } from '../../rootReducer'; import http from '../../services/api'; import { Diary } from '../../interfaces/diary.interface'; import { addDiary } from './diariesSlice'; import Swal from 'sweetalert2'; import { setUser } from '../auth/userSlice'; import DiaryTile from './DiaryTile'; import { User } from '../../interfaces/user.interface'; import { Route, Switch } from 'react-router-dom'; import DiaryEntriesList from './DiaryEntriesList'; import { useAppDispatch } from '../../store'; import dayjs from 'dayjs'; const Diaries: FC = () => { const dispatch = useAppDispatch(); const diaries = useSelector((state: RootState) => state.diaries); const user = useSelector((state: RootState) => state.user); useEffect(() => { const fetchDiaries = async () => { if (user) { http.get<null, Diary[]>(`diaries/${user.id}`).then((data) => { if (data && data.length > 0) { const sortedByUpdatedAt = data.sort((a, b) => { return dayjs(b.updatedAt).unix() - dayjs(a.updatedAt).unix(); }); dispatch(addDiary(sortedByUpdatedAt)); } }); } }; fetchDiaries(); }, [dispatch, user]); const createDiary = async () => { const result = await Swal.mixin({ input: 'text', confirmButtonText: 'Next →', showCancelButton: true, progressSteps: ['1', '2'], }).queue([ { titleText: 'Diary title', input: 'text', }, { titleText: 'Private or public diary?', input: 'radio', inputOptions: { private: 'Private', public: 'Public', }, inputValue: 'private', }, ]); if (result.value) { const { value } = result; const { diary, user: _user, } = await http.post<Partial<Diary>, { diary: Diary; user: User }>('/diaries/', { title: value[0], type: value[1], userId: user?.id, }); if (diary && user) { dispatch(addDiary([diary] as Diary[])); dispatch(addDiary([diary] as Diary[])); dispatch(setUser(_user)); return Swal.fire({ titleText: 'All done!', confirmButtonText: 'OK!', }); } } Swal.fire({ titleText: 'Cancelled', }); }; return ( <div style=> <Switch> <Route path="/diary/:id"> <DiaryEntriesList /> </Route> <Route path="/"> <button onClick={createDiary}>Create New</button> {diaries.map((diary, idx) => ( <DiaryTile key={idx} diary={diary} /> ))} </Route> </Switch> </div> ); }; export default Diaries;
In this component, we have a function to fetch the user’s diaries inside a useEffect hook, and a function to create a new diary. We also render our components in react-router’s <Route /> component, rendering a diary’s entries if its id matches the path param in the route /diary/:id, or otherwise rendering a list of the user’s diaries.
To wrap things up, let’s update the Home.tsx component. First, update the imports to look like the following:
import React, { FC } from 'react'; import Diaries from '../diary/Diaries'; import Editor from '../entry/Editor';
Then, change the component’s return statement to the following:
return ( <div className="two-cols"> <div className="left"> <Diaries /> </div> <div className="right"> <Editor /> </div> </div>
Finally, replace the contents of the index.css file in your app’s src directory with the following code:
:root { --primary-color: #778899; --error-color: #f85032; --text-color: #0d0d0d; --transition: all ease-in-out 0.3s; } body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } html, body, #root { height: 100%; } *, *:before, *:after { box-sizing: border-box; } .auth { display: flex; align-items: center; height: 100%; } .card { background: #fff; padding: 3rem; text-align: center; box-shadow: 2px 8px 12px rgba(0, 0, 0, 0.1); max-width: 450px; width: 90%; margin: 0 auto; } .inputWrapper { margin: 1rem auto; width: 100%; } input:not([type='checkbox']), button { border-radius: 0.5rem; width: 100%; } input:not([type='checkbox']), textarea { border: 2px solid rgba(0, 0, 0, 0.1); padding: 1em; color: var(--text-color); transition: var(--transition); } input:not([type='checkbox']):focus, textarea:focus { outline: none; border-color: var(--primary-color); } button { appearance: none; border: 1px solid var(--primary-color); color: #fff; background-color: var(--primary-color); text-transform: uppercase; font-weight: bold; outline: none; cursor: pointer; padding: 1em; box-shadow: 1px 4px 6px rgba(0, 0, 0, 0.1); transition: var(--transition); } button.secondary { color: var(--primary-color); background-color: #fff; border-color: #fff; } button:hover, button:focus { box-shadow: 1px 6px 8px rgba(0, 0, 0, 0.1); } .error { margin: 0; margin-top: 0.2em; font-size: 0.8em; color: var(--error-color); animation: 0.3s ease-in-out forwards fadeIn; } .two-cols { display: flex; flex-wrap: wrap; height: 100vh; } .two-cols .left { border-right: 1px solid rgba(0, 0, 0, 0.1); height: 100%; overflow-y: scroll; } .two-cols .right { overflow-y: auto; } .title { font-size: 1.3rem; } .subtitle { font-size: 0.9rem; opacity: 0.85; } .title, .subtitle { margin: 0; } .diary-tile { border-bottom: 1px solid rgba(0, 0, 0, 0.1); padding: 1em; } .editor { height: 100%; padding: 1em; } .editor input { width: 100%; } .editor textarea { width: 100%; height: calc(100vh - 160px); } .entries ul { list-style: none; padding: 0; } .entries li { border-top: 1px solid rgba(0, 0, 0, 0.1); padding: 0.5em; cursor: pointer; } .entries li:nth-child(even) { background: rgba(0, 0, 0, 0.1); } @media (min-width: 768px) { .two-cols .left { width: 25%; } .two-cols .right { width: 75%; } } @keyframes fadeIn { 0% { opacity: 0; } 100% { opacity: 0.8; } }
That’s it! You can now run npm start or yarn start and check out the final app at http://localhost:3000.
Final App Home Screen (Unauthenticated User). (Large preview)
Conclusion
In this guide, you have learned how to rapidly develop applications using RedUX. You also learned about good practices to follow when working with RedUX and React, in order to make debugging and extending your applications easier. This guide is by no means extensive as there are still ongoing discussions surrounding Redux and some of its concepts. Please check out the Redux and React-Redux docs if you’d like to learn more about using RedUX in your React projects.
References
(yk)
Website Design & SEO Delray Beach by DBL07.co
Delray Beach SEO
source http://www.scpie.org/setting-up-redux-for-use-in-a-real-world-application/ source https://scpie.tumblr.com/post/625506034270781440
0 notes
scpie · 5 years ago
Text
Setting Up Redux For Use In A Real-World Application
About The Author
I love building software for the web, writing about web technologies, and playing video games. More about Jerry …
RedUX is a robust state-management library for single-page Javascript apps. It is described on the official documentation as a predictable state container for Javascript applications and it’s fairly simple to learn the concepts and implement RedUX in a simple app. Going from a simple counter app to a real-world app, however, can be quite the jump.
RedUX is an important library in the React ecosystem, and almost the default to use when working on React applications that involve state management. As such, the importance of knowing how it works cannot be overestimated.
This guide will walk the reader through setting up RedUX in a fairly complex React application and introduce the reader to “best practices” configuration along the way. It will be beneficial to beginners especially, and anyone who wants to fill in the gaps in their knowledge of RedUX.
Introducing Redux
RedUX is a library that aims to solve the problem of state management in JavaScript apps by imposing restrictions on how and when state updates can happen. These restrictions are formed from RedUX’s “three principles” which are:
Single source of truth All of your application’s state is held in a RedUX store. This state can be represented visually as a tree with a single ancestor, and the store provides methods for reading the current state and subscribing to changes from anywhere within your app.
State is read-only The only way to change the state is to send the data as a plain object, called an action. You can think about actions as a way of saying to the state, “I have some data I would like to insert/update/delete”.
Changes are made with pure functions To change your app’s state, you write a function that takes the previous state and an action and returns a new state object as the next state. This function is called a reducer, and it is a pure function because it returns the same output for a given set of inputs.
The last principle is the most important in RedUX, and this is where the magic of RedUX happens. Reducer functions must not contain unpredictable code, or perform side-effects such as network requests, and should not directly mutate the state object.
RedUX is a great tool, as we’ll learn later in this guide, but it doesn’t come without its challenges or tradeoffs. To help make the process of writing RedUX efficient and more enjoyable, the RedUX team offers a toolkit that abstracts over the process of setting up a RedUX store and provides helpful RedUX add-ons and utilities that help to simplify application code. For example, the library uses Immer.js, a library that makes it possible for you to write “mutative” immutable update logic, under the hood.
Recommended reading: Better Reducers With Immer
In this guide, we will explore RedUX by building an application that lets authenticated users create and manage digital diaries.
Building Diaries.app
As stated in the previous section, we will be taking a closer look at RedUX by building an app that lets users create and manage diaries. We will be building our application using React, and we’ll set up Mirage as our API mocking server since we won’t have access to a real server in this guide.
Starting a Project and Installing Dependencies
Let’s get started on our project. First, bootstrap a new React application using create-react-app:
Using npx:
npx create-react-app diaries-app --template typescript
We are starting with the TypeScript template, as we can improve our development experience by writing type-safe code.
Now, let’s install the dependencies we’ll be needing. Navigate into your newly created project directory
cd diaries-app
And run the following commands:
npm install --save redUX react-redUX @redUXjs/toolkit
npm install --save axios react-router-dom react-hook-form yup dayjs markdown-to-jsx sweetalert2
npm install --save-dev miragejs @types/react-redUX @types/react-router-dom @types/yup @types/markdown-to-jsx
The first command will install RedUX, React-RedUX (official React bindings for RedUX), and the RedUX toolkit.
The second command installs some extra packages which will be useful for the app we’ll be building but are not required to work with RedUX.
The last command installs Mirage and type declarations for the packages we installed as devDependencies.
Describing the Application’s Initial State
Let’s go over our application’s requirements in detail. The application will allow authenticated users to create or modify existing diaries. Diaries are private by default, but they can be made public. Finally, diary entries will be sorted by their last modified date.
This relationship should look something like this:
An Overview of the Application’s Data Model. (Large preview)
Armed with this information, we can now model our application’s state. First, we will create an interface for each of the following resources: User, Diary and DiaryEntry. Interfaces in Typescript describe the shape of an object.
Go ahead and create a new directory named interfaces in your app’s src sub-directory:
cd src && mkdir interfaces
Next, run the following commands in the directory you just created:
touch entry.interface.ts touch diary.interface.ts touch user.interface.ts
This will create three files named entry.interface.ts, diary.interface.ts and user.interface.ts respectively. I prefer to keep interfaces that would be used in multiple places across my app in a single location.
Open entry.interface.ts and add the following code to set up the Entry interface:
export interface Entry { id?: string; title: string; content: string; createdAt?: string; updatedAt?: string; diaryId?: string; }
A typical diary entry will have a title and some content, as well as information about when it was created or last updated. We’ll get back to the diaryId property later.
Next, add the following to diary.interface.ts:
export interface Diary { id?: string; title: string; type: 'private' | 'public'; createdAt?: string; updatedAt?: string; userId?: string; entryIds: string[] | null; }
Here, we have a type property which expects an exact value of either ‘private’ or ‘public’, as diaries must be either private or public. Any other value will throw an error in the TypeScript compiler.
We can now describe our User object in the user.interface.ts file as follows:
export interface User { id?: string; username: string; email: string; password?: string; diaryIds: string[] | null; }
With our type definitions finished and ready to be used across our app, let’s setup our mock API server using Mirage.
Setting up API Mocking with MirageJS
Since this tutorial is focused on RedUX, we will not go into the details of setting up and using Mirage in this section. Please check out this excellent series if you would like to learn more about Mirage.
To get started, navigate to your src directory and create a file named server.ts by running the following commands:
mkdir -p services/mirage cd services/mirage # ~/diaries-app/src/services/mirage touch server.ts
Next, open the server.ts file and add the following code:
import { Server, Model, Factory, belongsTo, hasMany, Response } from 'miragejs'; export const handleErrors = (error: any, message = 'An error ocurred') => { return new Response(400, undefined, { data: { message, isError: true, }, }); }; export const setupServer = (env?: string): Server => { return new Server({ environment: env ?? 'development', models: { entry: Model.extend({ diary: belongsTo(), }), diary: Model.extend({ entry: hasMany(), user: belongsTo(), }), user: Model.extend({ diary: hasMany(), }), }, factories: { user: Factory.extend({ username: 'test', password: 'password', email: '[email protected]', }), }, seeds: (server): any => { server.create('user'); }, routes(): void { this.urlPrefix = 'https://diaries.app'; }, }); };
In this file, we are exporting two functions. A utility function for handling errors, and setupServer(), which returns a new server instance. The setupServer() function takes an optional argument which can be used to change the server’s environment. You can use this to set up Mirage for testing later.
We have also defined three models in the server’s models property: User, Diary and Entry. Remember that earlier we set up the Entry interface with a property named diaryId. This value will be automatically set to the id the entry is being saved to. Mirage uses this property to establish a relationship between an Entry and a Diary. The same thing also happens when a user creates a new diary: userId is automatically set to that user’s id.
We seeded the database with a default user and configured Mirage to intercept all requests from our app starting with https://diaries.app. Notice that we haven’t configured any route handlers yet. Let’s go ahead and create a few.
Ensure that you are in the src/services/mirage directory, then create a new directory named routes using the following command:
# ~/diaries-app/src/services/mirage mkdir routes
cd to the newly created directory and create a file named user.ts:
cd routes touch user.ts
Next, paste the following code in the user.ts file:
import { Response, Request } from 'miragejs'; import { handleErrors } from '../server'; import { User } from '../../../interfaces/user.interface'; import { randomBytes } from 'crypto'; const generateToken = () => randomBytes(8).toString('hex'); export interface AuthResponse { token: string; user: User; } const login = (schema: any, req: Request): AuthResponse | Response => { const { username, password } = JSON.parse(req.requestBody); const user = schema.users.findBy({ username }); if (!user) { return handleErrors(null, 'No user with that username exists'); } if (password !== user.password) { return handleErrors(null, 'Password is incorrect'); } const token = generateToken(); return { user: user.attrs as User, token, }; }; const signup = (schema: any, req: Request): AuthResponse | Response => { const data = JSON.parse(req.requestBody); const exUser = schema.users.findBy({ username: data.username }); if (exUser) { return handleErrors(null, 'A user with that username already exists.'); } const user = schema.users.create(data); const token = generateToken(); return { user: user.attrs as User, token, }; }; export default { login, signup, };
The login and signup methods here receive a Schema class and a fake Request object and, upon validating the password or checking that the login does not already exist, return the existing user or a new user respectively. We use the Schema object to interact with Mirage’s ORM, while the Request object contains information about the intercepted request including the request body and headers.
Next, let’s add methods for working with diaries and diary entries. Create a file named diary.ts in your routes directory:
touch diary.ts
Update the file with the following methods for working with Diary resources:
export const create = ( schema: any, req: Request ): { user: User; diary: Diary } | Response => { try { const { title, type, userId } = JSON.parse(req.requestBody) as Partial< Diary >; const exUser = schema.users.findBy({ id: userId }); if (!exUser) { return handleErrors(null, 'No such user exists.'); } const now = dayjs().format(); const diary = exUser.createDiary({ title, type, createdAt: now, updatedAt: now, }); return { user: { ...exUser.attrs, }, diary: diary.attrs, }; } catch (error) { return handleErrors(error, 'Failed to create Diary.'); } }; export const updateDiary = (schema: any, req: Request): Diary | Response => { try { const diary = schema.diaries.find(req.params.id); const data = JSON.parse(req.requestBody) as Partial<Diary>; const now = dayjs().format(); diary.update({ ...data, updatedAt: now, }); return diary.attrs as Diary; } catch (error) { return handleErrors(error, 'Failed to update Diary.'); } }; export const getDiaries = (schema: any, req: Request): Diary[] | Response => { try { const user = schema.users.find(req.params.id); return user.diary as Diary[]; } catch (error) { return handleErrors(error, 'Could not get user diaries.'); } };
Next, let’s add some methods for working with diary entries:
export const addEntry = ( schema: any, req: Request ): { diary: Diary; entry: Entry } | Response => { try { const diary = schema.diaries.find(req.params.id); const { title, content } = JSON.parse(req.requestBody) as Partial<Entry>; const now = dayjs().format(); const entry = diary.createEntry({ title, content, createdAt: now, updatedAt: now, }); diary.update({ ...diary.attrs, updatedAt: now, }); return { diary: diary.attrs, entry: entry.attrs, }; } catch (error) { return handleErrors(error, 'Failed to save entry.'); } }; export const getEntries = ( schema: any, req: Request ): { entries: Entry[] } | Response => { try { const diary = schema.diaries.find(req.params.id); return diary.entry; } catch (error) { return handleErrors(error, 'Failed to get Diary entries.'); } }; export const updateEntry = (schema: any, req: Request): Entry | Response => { try { const entry = schema.entries.find(req.params.id); const data = JSON.parse(req.requestBody) as Partial<Entry>; const now = dayjs().format(); entry.update({ ...data, updatedAt: now, }); return entry.attrs as Entry; } catch (error) { return handleErrors(error, 'Failed to update entry.'); } };
Finally, let’s add the necessary imports at the top of the file:
import { Response, Request } from 'miragejs'; import { handleErrors } from '../server'; import { Diary } from '../../../interfaces/diary.interface'; import { Entry } from '../../../interfaces/entry.interface'; import dayjs from 'dayjs'; import { User } from '../../../interfaces/user.interface';
In this file, we have exported methods for working with the Diary and Entry models. In the create method, we call a method named user.createDiary() to save a new diary and associate it to a user account.
The addEntry and updateEntry methods create and correctly associate a new entry to a diary or update an existing entry’s data respectively. The latter also updates the entry’s updatedAt property with the current timestamp. The updateDiary method also updates a diary with the timestamp the change was made. Later, we’ll be sorting the records we receive from our network request with this property.
We also have a getDiaries method which retrieves a user’s diaries and a getEntries methods which retrieves a selected diary’s entries.
We can now update our server to use the methods we just created. Open server.ts to include the files:
import { Server, Model, Factory, belongsTo, hasMany, Response } from 'miragejs'; import user from './routes/user'; import * as diary from './routes/diary';
Then, update the server’s route property with the routes we want to handle:
export const setupServer = (env?: string): Server => { return new Server({ // ... routes(): void { this.urlPrefix = 'https://diaries.app'; this.get('/diaries/entries/:id', diary.getEntries); this.get('/diaries/:id', diary.getDiaries); this.post('/auth/login', user.login); this.post('/auth/signup', user.signup); this.post('/diaries/', diary.create); this.post('/diaries/entry/:id', diary.addEntry); this.put('/diaries/entry/:id', diary.updateEntry); this.put('/diaries/:id', diary.updateDiary); }, }); };
With this change, when a network request from our app matches one of the route handlers, Mirage intercepts the request and invokes the respective route handler functions.
Next, we’ll proceed to make our application aware of the server. Open src/index.tsx and import the setupServer() method:
import { setupServer } from './services/mirage/server';
And add the following code before ReactDOM.render():
if (process.env.NODE_ENV === 'development') { setupServer(); }
The check in the code block above ensures that our Mirage server will run only while we are in development mode.
One last thing we need to do before moving on to the RedUX bits is configure a custom Axios instance for use in our app. This will help to reduce the amount of code we’ll have to write later on.
Create a file named api.ts under src/services and add the following code to it:
import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios'; import { showAlert } from '../util'; const http: AxiosInstance = axios.create({ baseURL: 'https://diaries.app', }); http.defaults.headers.post['Content-Type'] = 'application/json'; http.interceptors.response.use( async (response: AxiosResponse): Promise => { if (response.status >= 200 && response.status < 300) { return response.data; } }, (error: AxiosError) => { const { response, request }: { response?: AxiosResponse; request?: XMLHttpRequest; } = error; if (response) { if (response.status >= 400 && response.status < 500) { showAlert(response.data?.data?.message, 'error'); return null; } } else if (request) { showAlert('Request failed. Please try again.', 'error'); return null; } return Promise.reject(error); } ); export default http;
In this file, we are exporting an Axios instance modified to include our app’s API url, https://diaries.app. We have configured an interceptor to handle success and error responses, and we display error messages using a sweetalert toast which we will configure in the next step.
Create a file named util.ts in your src directory and paste the following code in it:
import Swal, { SweetAlertIcon } from 'sweetalert2'; export const showAlert = (titleText = 'Something happened.', alertType?: SweetAlertIcon): void => { Swal.fire({ titleText, position: 'top-end', timer: 3000, timerProgressBar: true, toast: true, showConfirmButton: false, showCancelButton: true, cancelButtonText: 'Dismiss', icon: alertType, showClass: { popup: 'swal2-noanimation', backdrop: 'swal2-noanimation', }, hideClass: { popup: '', backdrop: '', }, }); };
This file exports a function that displays a toast whenever it is invoked. The function accepts parameters to allow you set the toast message and type. For example, we are showing an error toast in the Axios response error interceptor like this:
showAlert(response.data?.data?.message, 'error');
Now when we make requests from our app while in development mode, they will be intercepted and handled by Mirage instead. In the next section, we will set up our RedUX store using RedUX toolkit.
Setting up a Redux Store
In this section, we are going to set up our store using the following exports from RedUX toolkit: configureStore(), getDefaultMiddleware() and createSlice(). Before we start, we should take a detailed look at what these exports do.
configureStore() is an abstraction over the RedUX createStore() function that helps simplify your code. It uses createStore() internally to set up your store with some useful development tools:
export const store = configureStore({ reducer: rootReducer, // a single reducer function or an object of slice reducers });
The createSlice() function helps simplify the process of creating action creators and slice reducers. It accepts an initial state, an object full of reducer functions, and a “slice name”, and automatically generates action creators and action types corresponding to the reducers and your state. It also returns a single reducer function, which can be passed to RedUX’s combineReducers() function as a “slice reducer”.
Remember that the state is a single tree, and a single root reducer manages changes to that tree. For maintainability, it is recommended to split your root reducer into “slices,” and have a “slice reducer” provide an initial value and calculate the updates to a corresponding slice of the state. These slices can be joined into a single reducer function by using combineReducers().
There are additional options for configuring the store. For example, you can pass an array of your own middleware to configureStore() or start up your app from a saved state using the preloadedState option. When you supply the middleware option, you have to define all the middleware you want added to the store. If you would like to retain the defaults when setting up your store, you can use getDefaultMiddleware() to get the default list of middleware:
export const store = configureStore({ // ... middleware: [...getDefaultMiddleware(), customMiddleware], });
Let’s now proceed to set up our store. We will adopt a “ducks-style” approach to structuring our files, specifically following the guidelines in practice from the Github Issues sample app. We will be organizing our code such that related components, as well as actions and reducers, live in the same directory. The final state object will look like this:
type RootState = { auth: { token: string | null; isAuthenticated: boolean; }; diaries: Diary[]; entries: Entry[]; user: User | null; editor: { canEdit: boolean; currentlyEditing: Entry | null; activeDiaryId: string | null; }; }
To get started, create a new directory named features under your src directory:
# ~/diaries-app/src mkdir features
Then, cd into features and create directories named auth, diary and entry:
cd features mkdir auth diary entry
cd into the auth directory and create a file named authSlice.ts:
cd auth # ~/diaries-app/src/features/auth touch authSlice.ts
Open the file and paste the following in it:
import { createSlice, PayloadAction } from '@redUXjs/toolkit'; interface AuthState { token: string | null; isAuthenticated: boolean; } const initialState: AuthState = { token: null, isAuthenticated: false, }; const auth = createSlice({ name: 'auth', initialState, reducers: { saveToken(state, { payload }: PayloadAction) { if (payload) { state.token = payload; } }, clearToken(state) { state.token = null; }, setAuthState(state, { payload }: PayloadAction) { state.isAuthenticated = payload; }, }, }); export const { saveToken, clearToken, setAuthState } = auth.actions; export default auth.reducer;
In this file, we’re creating a slice for the auth property of our app’s state using the createSlice() function introduced earlier. The reducers property holds a map of reducer functions for updating values in the auth slice. The returned object contains automatically generated action creators and a single slice reducer. We would need to use these in other files so, following the “ducks pattern”, we do named exports of the action creators, and a default export of the reducer function.
Let’s set up the remaining reducer slices according to the app state we saw earlier. First, create a file named userSlice.ts in the auth directory and add the following code to it:
import { createSlice, PayloadAction } from '@redUXjs/toolkit'; import { User } from '../../interfaces/user.interface'; const user = createSlice({ name: 'user', initialState: null as User | null, reducers: { setUser(state, { payload }: PayloadAction<User | null>) { return state = (payload != null) ? payload : null; }, }, }); export const { setUser } = user.actions; export default user.reducer;
This creates a slice reducer for the user property in our the application’s store. The setUser reducer function accepts a payload containing user data and updates the state with it. When no data is passed, we set the state’s user property to null.
Next, create a file named diariesSlice.ts under src/features/diary:
# ~/diaries-app/src/features cd diary touch diariesSlice.ts
Add the following code to the file:
import { createSlice, PayloadAction } from '@redUXjs/toolkit'; import { Diary } from '../../interfaces/diary.interface'; const diaries = createSlice({ name: 'diaries', initialState: [] as Diary[], reducers: { addDiary(state, { payload }: PayloadAction<Diary[]>) { const diariesToSave = payload.filter((diary) => { return state.findIndex((item) => item.id === diary.id) === -1; }); state.push(...diariesToSave); }, updateDiary(state, { payload }: PayloadAction<Diary>) { const { id } = payload; const diaryIndex = state.findIndex((diary) => diary.id === id); if (diaryIndex !== -1) { state.splice(diaryIndex, 1, payload); } }, }, }); export const { addDiary, updateDiary } = diaries.actions; export default diaries.reducer;
The “diaries” property of our state is an array containing the user’s diaries, so our reducer functions here all work on the state object they receive using array methods. Notice here that we are writing normal “mutative” code when working on the state. This is possible because the reducer functions we create using the createSlice() method are wrapped with Immer’s produce() method. This results in Immer returning a correct immutably updated result for our state regardless of us writing mutative code.
Next, create a file named entriesSlice.ts under src/features/entry:
# ~/diaries-app/src/features mkdir entry cd entry touch entriesSlice.ts
Open the file and add the following code:
import { createSlice, PayloadAction } from '@redUXjs/toolkit'; import { Entry } from '../../interfaces/entry.interface'; const entries = createSlice({ name: 'entries', initialState: [] as Entry[], reducers: { setEntries(state, { payload }: PayloadAction<Entry[] | null>) { return (state = payload != null ? payload : []); }, updateEntry(state, { payload }: PayloadAction<Entry>) { const { id } = payload; const index = state.findIndex((e) => e.id === id); if (index !== -1) { state.splice(index, 1, payload); } }, }, }); export const { setEntries, updateEntry } = entries.actions; export default entries.reducer;
The reducer functions here have logic similar to the previous slice’s reducer functions. The entries property is also an array, but it only holds entries for a single diary. In our app, this will be the diary currently in the user’s focus.
Finally, create a file named editorSlice.ts in src/features/entry and add the following to it:
import { createSlice, PayloadAction } from '@redUXjs/toolkit'; import { Entry } from '../../interfaces/entry.interface'; interface EditorState { canEdit: boolean; currentlyEditing: Entry | null; activeDiaryId: string | null; } const initialState: EditorState = { canEdit: false, currentlyEditing: null, activeDiaryId: null, }; const editor = createSlice({ name: 'editor', initialState, reducers: { setCanEdit(state, { payload }: PayloadAction<boolean>) { state.canEdit = payload != null ? payload : !state.canEdit; }, setCurrentlyEditing(state, { payload }: PayloadAction<Entry | null>) { state.currentlyEditing = payload; }, setActiveDiaryId(state, { payload }: PayloadAction<string>) { state.activeDiaryId = payload; }, }, }); export const { setCanEdit, setCurrentlyEditing, setActiveDiaryId } = editor.actions; export default editor.reducer;
Here, we have a slice for the editor property in state. We’ll be using the properties in this object to check if the user wants to switch to editing mode, which diary the edited entry belongs to, and what entry is going to be edited.
To put it all together, create a file named rootReducer.ts in the src directory with the following content:
import { combineReducers } from '@redUXjs/toolkit'; import authReducer from './features/auth/authSlice'; import userReducer from './features/auth/userSlice'; import diariesReducer from './features/diary/diariesSlice'; import entriesReducer from './features/entry/entriesSlice'; import editorReducer from './features/entry/editorSlice'; const rootReducer = combineReducers({ auth: authReducer, diaries: diariesReducer, entries: entriesReducer, user: userReducer, editor: editorReducer, }); export type RootState = ReturnType<typeof rootReducer>; export default rootReducer;
In this file, we’ve combined our slice reducers into a single root reducer with the combineReducers() function. We’ve also exported the RootState type, which will be useful later when we’re selecting values from the store. We can now use the root reducer (the default export of this file) to set up our store.
Create a file named store.ts with the following contents:
import { configureStore } from '@redUXjs/toolkit'; import rootReducer from './rootReducer'; import { useDispatch } from 'react-redUX'; const store = configureStore({ reducer: rootReducer, }); type AppDispatch = typeof store.dispatch; export const useAppDispatch = () => useDispatch<AppDispatch>(); export default store;
With this, we’ve created a store using the configureStore() export from RedUX toolkit. We’ve also exported an hook called useAppDispatch() which merely returns a typed useDispatch() hook.
Next, update the imports in index.tsx to look like the following:
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './app/App'; import * as serviceWorker from './serviceWorker'; import { setupServer } from './services/mirage/server'; import { Provider } from 'react-redUX'; import store from './store'; // ...
Finally, make the store available to the app’s components by wrapping <App /> (the top-level component) with <Provider />:
ReactDOM.render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode>, document.getElementById('root') );
Now, if you start your app and you navigate to http://localhost:3000 with the Redux Dev Tools extension enabled, you should see the following in your app’s state:
Initial State in RedUX Dev Tools Extension. (Large preview)
Great work so far, but we’re not quite finished yet. In the next section, we will design the app’s User Interface and add functionality using the store we’ve just created.
Designing The Application User Interface
To see RedUX in action, we are going to build a demo app. In this section, we will connect our components to the store we’ve created and learn to dispatch actions and modify the state using reducer functions. We will also learn how to read values from the store. Here’s what our RedUX-powered application will look like.
Home page showing an authenticated user’s diaries. (Large preview)
Screenshots of final app. (Large preview)
Setting up the Authentication Feature
To get started, move App.tsx and its related files from the src directory to its own directory like this:
# ~/diaries-app/src mkdir app mv App.tsx App.test.tsx app
You can delete the App.css and logo.svg files as we won’t be needing them.
Next, open the App.tsx file and replace its contents with the following:
import React, { FC, lazy, Suspense } from 'react'; import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; import { useSelector } from 'react-redUX'; import { RootState } from '../rootReducer'; const Auth = lazy(() => import('../features/auth/Auth')); const Home = lazy(() => import('../features/home/Home')); const App: FC = () => { const isLoggedIn = useSelector( (state: RootState) => state.auth.isAuthenticated ); return ( <Router> <Switch> <Route path="/"> <Suspense fallback={<p>Loading...</p>}> {isLoggedIn ? <Home /> : <Auth />} </Suspense> </Route> </Switch> </Router> ); }; export default App;
Here we have set up our app to render an <Auth /> component if the user is unauthenticated, or otherwise render a <Home /> component. We haven’t created either of these components yet, so let’s fix that. Create a file named Auth.tsx under src/features/auth and add the following contents to the file:
import React, { FC, useState } from 'react'; import { useForm } from 'react-hook-form'; import { User } from '../../interfaces/user.interface'; import * as Yup from 'yup'; import http from '../../services/api'; import { saveToken, setAuthState } from './authSlice'; import { setUser } from './userSlice'; import { AuthResponse } from '../../services/mirage/routes/user'; import { useAppDispatch } from '../../store'; const schema = Yup.object().shape({ username: Yup.string() .required('What? No username?') .max(16, 'Username cannot be longer than 16 characters'), password: Yup.string().required('Without a password, "None shall pass!"'), email: Yup.string().email('Please provide a valid email address ([email protected])'), }); const Auth: FC = () => { const { handleSubmit, register, errors } = useForm<User>({ validationSchema: schema, }); const [isLogin, setIsLogin] = useState(true); const [loading, setLoading] = useState(false); const dispatch = useAppDispatch(); const submitForm = (data: User) => { const path = isLogin ? '/auth/login' : '/auth/signup'; http .post<User, AuthResponse>(path, data) .then((res) => { if (res) { const { user, token } = res; dispatch(saveToken(token)); dispatch(setUser(user)); dispatch(setAuthState(true)); } }) .catch((error) => { console.log(error); }) .finally(() => { setLoading(false); }); }; return ( <div className="auth"> <div className="card"> <form onSubmit={handleSubmit(submitForm)}> <div className="inputWrapper"> <input ref={register} name="username" placeholder="Username" /> {errors && errors.username && ( <p className="error">{errors.username.message}</p> )} </div> <div className="inputWrapper"> <input ref={register} name="password" type="password" placeholder="Password" /> {errors && errors.password && ( <p className="error">{errors.password.message}</p> )} </div> {!isLogin && ( <div className="inputWrapper"> <input ref={register} name="email" placeholder="Email (optional)" /> {errors && errors.email && ( <p className="error">{errors.email.message}</p> )} </div> )} <div className="inputWrapper"> <button type="submit" disabled={loading}> {isLogin ? 'Login' : 'Create account'} </button> </div> <p onClick={() => setIsLogin(!isLogin)} style= > {isLogin ? 'No account? Create one' : 'Already have an account?'} </p> </form> </div> </div> ); }; export default Auth;
In this component, we have set up a form for users to log in, or to create an account. Our form fields are validated using Yup and, on successfully authenticating a user, we use our useAppDispatch hook to dispatch the relevant actions. You can see the dispatched actions and the changes made to your state in the RedUX DevTools Extension:
Dispatched Actions with Changes Tracked in RedUX Dev Tools Extensions. (Large preview)
Finally, create a file named Home.tsx under src/features/home and add the following code to the file:
import React, { FC } from 'react'; const Home: FC = () => { return ( <div> <p>Welcome user!</p> </div> ); }; export default Home;
For now, we are just displaying some text to the authenticated user. As we build the rest of our application, we will be updating this file.
Setting up the Editor
The next component we are going to build is the editor. Though basic, we will enable support for rendering markdown content using the markdown-to-jsx library we installed earlier.
First, create a file named Editor.tsx in the src/features/entry directory. Then, add the following code to the file:
import React, { FC, useState, useEffect } from 'react'; import { useSelector } from 'react-redUX'; import { RootState } from '../../rootReducer'; import Markdown from 'markdown-to-jsx'; import http from '../../services/api'; import { Entry } from '../../interfaces/entry.interface'; import { Diary } from '../../interfaces/diary.interface'; import { setCurrentlyEditing, setCanEdit } from './editorSlice'; import { updateDiary } from '../diary/diariesSlice'; import { updateEntry } from './entriesSlice'; import { showAlert } from '../../util'; import { useAppDispatch } from '../../store'; const Editor: FC = () => { const { currentlyEditing: entry, canEdit, activeDiaryId } = useSelector( (state: RootState) => state.editor ); const [editedEntry, updateEditedEntry] = useState(entry); const dispatch = useAppDispatch(); const saveEntry = async () => { if (activeDiaryId == null) { return showAlert('Please select a diary.', 'warning'); } if (entry == null) { http .post<Entry, { diary: Diary; entry: Entry }>( `/diaries/entry/${activeDiaryId}`, editedEntry ) .then((data) => { if (data != null) { const { diary, entry: _entry } = data; dispatch(setCurrentlyEditing(_entry)); dispatch(updateDiary(diary)); } }); } else { http .put<Entry, Entry>(`diaries/entry/${entry.id}`, editedEntry) .then((_entry) => { if (_entry != null) { dispatch(setCurrentlyEditing(_entry)); dispatch(updateEntry(_entry)); } }); } dispatch(setCanEdit(false)); }; useEffect(() => { updateEditedEntry(entry); }, [entry]); return ( <div className="editor"> <header style= > {entry && !canEdit ? ( <h4> {entry.title} <a href="#edit" onClick={(e) => { e.preventDefault(); if (entry != null) { dispatch(setCanEdit(true)); } }} style= > (Edit) </a> </h4> ) : ( <input value={editedEntry?.title ?? ''} disabled={!canEdit} onChange={(e) => { if (editedEntry) { updateEditedEntry({ ...editedEntry, title: e.target.value, }); } else { updateEditedEntry({ title: e.target.value, content: '', }); } }} /> )} </header> {entry && !canEdit ? ( <Markdown>{entry.content}</Markdown> ) : ( <> <textarea disabled={!canEdit} placeholder="Supports markdown!" value={editedEntry?.content ?? ''} onChange={(e) => { if (editedEntry) { updateEditedEntry({ ...editedEntry, content: e.target.value, }); } else { updateEditedEntry({ title: '', content: e.target.value, }); } }} /> <button onClick={saveEntry} disabled={!canEdit}> Save </button> </> )} </div> ); }; export default Editor;
Let’s break down what’s happening in the Editor component.
First, we are picking some values (with correctly inferred types) from the app’s state using the useSelector() hook from react-redUX. In the next line, we have a stateful value called editedEntry whose initial value is set to the editor.currentlyEditing property we’ve selected from the store.
Next, we have the saveEntry function which updates or creates a new entry in the API, and dispatches the respective RedUX action.
Finally, we have a useEffect that is fired when the editor.currentlyEditing property changes. Our editor’s UI (in the component’s return function) has been set up to respond to changes in the state. For example, rendering the entry’s content as JSX elements when the user isn’t editing.
With that, the app’s Entry feature should be completely set up. In the next section, we will finish building the Diary feature and then import the main components in the Home component we created earlier.
Final Steps
To finish up our app, we will first create components for the Diary feature. Then, we will update the Home component with the primary exports from the Diary and Entry features. Finally, we will add some styling to give our app the required pizzazz!
Let’s start by creating a file in src/features/diary named DiaryTile.tsx. This component will present information about a diary and its entries, and allow the user to edit the diary’s title. Add the following code to the file:
import React, { FC, useState } from 'react'; import { Diary } from '../../interfaces/diary.interface'; import http from '../../services/api'; import { updateDiary } from './diariesSlice'; import { setCanEdit, setActiveDiaryId, setCurrentlyEditing } from '../entry/editorSlice'; import { showAlert } from '../../util'; import { Link } from 'react-router-dom'; import { useAppDispatch } from '../../store'; interface Props { diary: Diary; } const buttonStyle: React.CSSProperties = { fontSize: '0.7em', margin: '0 0.5em', }; const DiaryTile: FC<Props> = (props) => { const [diary, setDiary] = useState(props.diary); const [isEditing, setIsEditing] = useState(false); const dispatch = useAppDispatch(); const totalEntries = props.diary?.entryIds?.length; const saveChanges = () => { http .put<Diary, Diary>(`/diaries/${diary.id}`, diary) .then((diary) => { if (diary) { dispatch(updateDiary(diary)); showAlert('Saved!', 'success'); } }) .finally(() => { setIsEditing(false); }); }; return ( <div className="diary-tile"> <h2 className="title" title="Click to edit" onClick={() => setIsEditing(true)} style= > {isEditing ? ( <input value={diary.title} onChange={(e) => { setDiary({ ...diary, title: e.target.value, }); }} onKeyUp={(e) => { if (e.key === 'Enter') { saveChanges(); } }} /> ) : ( <span>{diary.title}</span> )} </h2> <p className="subtitle">{totalEntries ?? '0'} saved entries</p> <div style=> <button style={buttonStyle} onClick={() => { dispatch(setCanEdit(true)); dispatch(setActiveDiaryId(diary.id as string)); dispatch(setCurrentlyEditing(null)); }} > Add New Entry </button> <Link to={`diary/${diary.id}`} style=> <button className="secondary" style={buttonStyle}> View all → </button> </Link> </div> </div> ); }; export default DiaryTile;
In this file, we receive a diary object as a prop and display the data in our component. Notice that we use local state and component props for our data display here. That’s because you don’t have to manage all your app’s state using RedUX. Sharing data using props, and maintaining local state in your components is acceptable and encouraged in some cases.
Next, let’s create a component that will display a list of a diary’s entries, with the last updated entries at the top of the list. Ensure you are in the src/features/diary directory, then create a file named DiaryEntriesList.tsx and add the following code to the file:
import React, { FC, useEffect } from 'react'; import { useParams, Link } from 'react-router-dom'; import { useSelector } from 'react-redUX'; import { RootState } from '../../rootReducer'; import http from '../../services/api'; import { Entry } from '../../interfaces/entry.interface'; import { setEntries } from '../entry/entriesSlice'; import { setCurrentlyEditing, setCanEdit } from '../entry/editorSlice'; import dayjs from 'dayjs'; import { useAppDispatch } from '../../store'; const DiaryEntriesList: FC = () => { const { entries } = useSelector((state: RootState) => state); const dispatch = useAppDispatch(); const { id } = useParams(); useEffect(() => { if (id != null) { http .get<null, { entries: Entry[] }>(`/diaries/entries/${id}`) .then(({ entries: _entries }) => { if (_entries) { const sortByLastUpdated = _entries.sort((a, b) => { return dayjs(b.updatedAt).unix() - dayjs(a.updatedAt).unix(); }); dispatch(setEntries(sortByLastUpdated)); } }); } }, [id, dispatch]); return ( <div className="entries"> <header> <Link to="/"> <h3>← Go Back</h3> </Link> </header> <ul> {entries.map((entry) => ( <li key={entry.id} onClick={() => { dispatch(setCurrentlyEditing(entry)); dispatch(setCanEdit(true)); }} > {entry.title} </li> ))} </ul> </div> ); }; export default DiaryEntriesList;
Here, we subscribe to the entries property of our app’s state, and have our effect fetch a diary’s entry only run when a property, id, changes. This property’s value is gotten from our URL as a path parameter using the useParams() hook from react-router. In the next step, we will create a component that will enable users to create and view diaries, as well as render a diary’s entries when it is in focus.
Create a file named Diaries.tsx while still in the same directory, and add the following code to the file:
import React, { FC, useEffect } from 'react'; import { useSelector } from 'react-redUX'; import { RootState } from '../../rootReducer'; import http from '../../services/api'; import { Diary } from '../../interfaces/diary.interface'; import { addDiary } from './diariesSlice'; import Swal from 'sweetalert2'; import { setUser } from '../auth/userSlice'; import DiaryTile from './DiaryTile'; import { User } from '../../interfaces/user.interface'; import { Route, Switch } from 'react-router-dom'; import DiaryEntriesList from './DiaryEntriesList'; import { useAppDispatch } from '../../store'; import dayjs from 'dayjs'; const Diaries: FC = () => { const dispatch = useAppDispatch(); const diaries = useSelector((state: RootState) => state.diaries); const user = useSelector((state: RootState) => state.user); useEffect(() => { const fetchDiaries = async () => { if (user) { http.get<null, Diary[]>(`diaries/${user.id}`).then((data) => { if (data && data.length > 0) { const sortedByUpdatedAt = data.sort((a, b) => { return dayjs(b.updatedAt).unix() - dayjs(a.updatedAt).unix(); }); dispatch(addDiary(sortedByUpdatedAt)); } }); } }; fetchDiaries(); }, [dispatch, user]); const createDiary = async () => { const result = await Swal.mixin({ input: 'text', confirmButtonText: 'Next →', showCancelButton: true, progressSteps: ['1', '2'], }).queue([ { titleText: 'Diary title', input: 'text', }, { titleText: 'Private or public diary?', input: 'radio', inputOptions: { private: 'Private', public: 'Public', }, inputValue: 'private', }, ]); if (result.value) { const { value } = result; const { diary, user: _user, } = await http.post<Partial<Diary>, { diary: Diary; user: User }>('/diaries/', { title: value[0], type: value[1], userId: user?.id, }); if (diary && user) { dispatch(addDiary([diary] as Diary[])); dispatch(addDiary([diary] as Diary[])); dispatch(setUser(_user)); return Swal.fire({ titleText: 'All done!', confirmButtonText: 'OK!', }); } } Swal.fire({ titleText: 'Cancelled', }); }; return ( <div style=> <Switch> <Route path="/diary/:id"> <DiaryEntriesList /> </Route> <Route path="/"> <button onClick={createDiary}>Create New</button> {diaries.map((diary, idx) => ( <DiaryTile key={idx} diary={diary} /> ))} </Route> </Switch> </div> ); }; export default Diaries;
In this component, we have a function to fetch the user’s diaries inside a useEffect hook, and a function to create a new diary. We also render our components in react-router’s <Route /> component, rendering a diary’s entries if its id matches the path param in the route /diary/:id, or otherwise rendering a list of the user’s diaries.
To wrap things up, let’s update the Home.tsx component. First, update the imports to look like the following:
import React, { FC } from 'react'; import Diaries from '../diary/Diaries'; import Editor from '../entry/Editor';
Then, change the component’s return statement to the following:
return ( <div className="two-cols"> <div className="left"> <Diaries /> </div> <div className="right"> <Editor /> </div> </div>
Finally, replace the contents of the index.css file in your app’s src directory with the following code:
:root { --primary-color: #778899; --error-color: #f85032; --text-color: #0d0d0d; --transition: all ease-in-out 0.3s; } body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } html, body, #root { height: 100%; } *, *:before, *:after { box-sizing: border-box; } .auth { display: flex; align-items: center; height: 100%; } .card { background: #fff; padding: 3rem; text-align: center; box-shadow: 2px 8px 12px rgba(0, 0, 0, 0.1); max-width: 450px; width: 90%; margin: 0 auto; } .inputWrapper { margin: 1rem auto; width: 100%; } input:not([type='checkbox']), button { border-radius: 0.5rem; width: 100%; } input:not([type='checkbox']), textarea { border: 2px solid rgba(0, 0, 0, 0.1); padding: 1em; color: var(--text-color); transition: var(--transition); } input:not([type='checkbox']):focus, textarea:focus { outline: none; border-color: var(--primary-color); } button { appearance: none; border: 1px solid var(--primary-color); color: #fff; background-color: var(--primary-color); text-transform: uppercase; font-weight: bold; outline: none; cursor: pointer; padding: 1em; box-shadow: 1px 4px 6px rgba(0, 0, 0, 0.1); transition: var(--transition); } button.secondary { color: var(--primary-color); background-color: #fff; border-color: #fff; } button:hover, button:focus { box-shadow: 1px 6px 8px rgba(0, 0, 0, 0.1); } .error { margin: 0; margin-top: 0.2em; font-size: 0.8em; color: var(--error-color); animation: 0.3s ease-in-out forwards fadeIn; } .two-cols { display: flex; flex-wrap: wrap; height: 100vh; } .two-cols .left { border-right: 1px solid rgba(0, 0, 0, 0.1); height: 100%; overflow-y: scroll; } .two-cols .right { overflow-y: auto; } .title { font-size: 1.3rem; } .subtitle { font-size: 0.9rem; opacity: 0.85; } .title, .subtitle { margin: 0; } .diary-tile { border-bottom: 1px solid rgba(0, 0, 0, 0.1); padding: 1em; } .editor { height: 100%; padding: 1em; } .editor input { width: 100%; } .editor textarea { width: 100%; height: calc(100vh - 160px); } .entries ul { list-style: none; padding: 0; } .entries li { border-top: 1px solid rgba(0, 0, 0, 0.1); padding: 0.5em; cursor: pointer; } .entries li:nth-child(even) { background: rgba(0, 0, 0, 0.1); } @media (min-width: 768px) { .two-cols .left { width: 25%; } .two-cols .right { width: 75%; } } @keyframes fadeIn { 0% { opacity: 0; } 100% { opacity: 0.8; } }
That’s it! You can now run npm start or yarn start and check out the final app at http://localhost:3000.
Final App Home Screen (Unauthenticated User). (Large preview)
Conclusion
In this guide, you have learned how to rapidly develop applications using RedUX. You also learned about good practices to follow when working with RedUX and React, in order to make debugging and extending your applications easier. This guide is by no means extensive as there are still ongoing discussions surrounding Redux and some of its concepts. Please check out the Redux and React-Redux docs if you’d like to learn more about using RedUX in your React projects.
References
(yk)
Website Design & SEO Delray Beach by DBL07.co
Delray Beach SEO
source http://www.scpie.org/setting-up-redux-for-use-in-a-real-world-application/
0 notes
inthetechpit · 5 years ago
Text
Authorize .net core webapi with token using Windows Authentication
Authorize .net core webapi with token using Windows Authentication
Create a TokenFactory class that creates an encrypted token with the Windows authenticated username:
public class TokenFactory { public static Aes aes = Aes.Create(); public static string GenerateToken(string UserName) { //Get the required values... TokenDetails encrytionData = new TokenDetails { UserName = uName, Role = rName, SecretKey = sKey, Date = DateTime.Now, Interval = duration }; //To…
View On WordPress
0 notes
tak4hir0 · 6 years ago
Link
本書では OAuth2 で定義されたRefresh Tokenの概念について学びます。また、Refresh Tokenと他のトークンタイプを比較して、その理由と方法を学びます。さらに、簡単な例を使ってRefresh Tokenの使い方について説明します。それでは、始めましょう! 更新: 本書を書いた時点では、Auth0 は OpenID Connect 認証を取得していませんでした。本書では access token のような用語の一部は本仕様に準拠しませんが、 OAuth2 仕様には準拠しています。OpenID Connect は access token (Authorization Server[認証サーバー]の API へのアクセスに使用)および id token (リソース サーバーに対するクライアント認証に使用)を明確に区別します。 はじめに 先進的な認証および/または認可ソリューションはトークンの概念をそのプロトコルに導入しました。トークンは具体的に、 ユーザーがアクションを実行することを認可するか承認 、もしくはクライアントが 認可プロセスに関する追加情報を取得 し、完了するために十分な情報を含む特別に加工されたデータです。言い換えれば、トークンは認可プロセスが実行することを可能にする情報の構成物です。この情報がクライアント(または、Authorization Server以外のパーティ)によって読み取り可能か解析可能かは、実装によって定義されます。重要な点は、クライアントがこの情報を取得し、トークンを リソースへのアクセスを するために使用することです。JSON Web Token (JWT) 仕様は共通のトークン情報が実装によって表される方法を定義します。 JWT の 要約 簡単なまとめ JWT は認証/認可プロセスに関する特定の共通情報を 表記する 方法を定義しています。名前が示す通り、データ形式は JSON です。JWT はsubject(件名)、issuer(発行元)、expiration time(有効期限)など特定の 共通フィールド を持ちます。JWTは、 JSON Web Signature (JWS)や JSON Web Encryption (JWE)などのその他の仕様と組み合わせたときに非常に役に立ちます。これらの仕様を組み合わせることで、認証トークンに必要な全ての情報を提供するだけでなく、改ざんされなくなるようにトークンの コンテンツを検証 する機能 (JWS)や、クライアントに対して 不透明さ を維持できるように 情報を暗号化 する機能(JWT)を提供します。データ形式のシンプルさ(とその他の長所)のお陰で、JWT はトークンの最も一般的タイプのひとつになりました。JWT を Web アプリに実装する方法については、Ryan Chenkie 氏の素晴らしい 投稿をご覧ください。 トークンのタイプ 本書の目的上、最も一般的タイプのトークン、Access Token および Refresh Token の 2 つのタイプについて学びます。 Access Token はリソースに直接アクセスするために必要な情報を保持しています。つまり、クライアントがリソースを管理するサーバーにAccess Tokenをパスするとき、そのサーバーはそのトークンに含まれている情報を使用してクライアントが認可したものかを判断します。Access Tokenには通常、有効期限があり、存続期間は短いです。 Refresh Token は新しいAccess Tokenを取得するために必要な情報を保持しています。つまり、特定リソースにアクセスする際に、Access Tokenが必要な場合には、クライアントはAuthorization Serverが発行する新しいAccess Tokenを取得するためにRefresh Tokenを使用します。一般的な使用方法は、Access Tokenの期限が切れた後に新しいものを取得したり、初めて新しいリソースにアクセスするときなどです。Refresh Tokenにも有効期限がありますが、存続期間はAccess Tokenよりも長くなっています。Refresh Tokenは通常、漏洩しないように厳しいストレージ要件が課せられます。Authorization Serverによってブラックリストに載ることもあります。 トークンが不透明かどうかは通常、実装によって定義されます。一般的な実装は、 Access Token に対する 直接認可 チェックを許可にします。つまり、Access Tokenがリソースを管理するサーバーに渡されると、そのサーバーはそのトークンに含まれる情報を読み取り、ユーザーが認可されているかを独自に判断します(Authorization Serverに対するチェックは不要です)。これが、トークンが署名されなければならない理由のひとつです(例えば、JWS を使う)。一方、Refresh Tokenは通常、Authorization Serverに対するチェックを要します。認可チェックの処理を分割することで、次の 3 つが可能になります。 Authorization Serverに対するアクセスパターンの改善(負荷の軽減、迅速なチェック) Access Tokenの漏洩に対する短い有効期限(これらの有効期限が短いことで、漏洩したトークンを使用して保護されたリソースへのアクセスが許可されてしまう可能性が低減されます) スライディング セッション(以下参照) スライディング セッション スライディング セッションとは、 一定期間使用しない と期限切れになるセッションです。予想される通り、これはAccess TokenとRefresh Tokenを使って簡単に実装できます。ユーザーがあるアクションを実行すると、新しいAccess Tokenが発行されます。このユーザーが期限切れのAccess Tokenを使用すると、そのセッションは非アクティブと見なされて新しいAccess Tokenが必要になります。このトークンがRefresh Tokenで取得できるか、新しい認証ラウンドが必要となるかは、開発チームの要件によって定義されます。 セキュリティの考慮事項 Refresh Tokenは 有効 期間が長い です。そのため、クライアントがサーバーからRefresh Tokenを取得した後、このトークンを潜在的な攻撃者が使用できないように、セキュリティで保護 されなければなりません。Refresh Tokenが漏洩してしまうと、それがブラックリストに載るまで、あるいは期限切れになるまで(このようになるには時間を要するかもしれません)、新しいAccess Tokenを取得するために(そして、保護されたリソースにアクセスするために)使用されるかもしれません。Refresh Tokenは他のパーティが漏洩されたトークンを使用しないように、ひとつの認証クライアントに発行されなければなりません。Access Tokenは秘密にしなければなりませんが、存続期間が短いため、予想される通り、セキュリティの考慮事項の制限も緩くなります。 「Access Tokenは秘密にしなければなりませんが、存続期間が短いため、セキュリティの考慮事項の制限も緩くなります。」 これをツイートする 例:サーバー発行のRefresh Token この例では、Access TokenとRefresh Tokenを発行するために node-oauth2-server がベースの簡単なサーバーを使用します。Access Tokenは、保護されているリソースにアクセスするために必要となります。クライアントはシンプルな CURL コマンドです。この例のコードは node-oauth2-server の例をベースにしています。Access Tokenを JWT で使用するためにベース例を修正しました。 Node-oauth2-server ��モデル用事前定義されたAPI を使用します。このドキュメントは こちらからご覧ください。以下のコードは JWT Access Tokenのモデルを実装する方法を示しています。 DISCLAIMER: 以下の例にあるコードは本番環境用ではありませんので、ご注意ください。 model.generateToken = function(type, req, callback) { //refresh tokens に既定の実装を使用します console.log('generateToken: ' + type); if(type === 'refreshToken') { callback(null, null); return; } //access tokens に JWT を使用します var token = jwt.sign({ user: req.user.id }, secretKey, { expiresIn: model.accessTokenLifetime, subject: req.client.clientId }); callback(null, token); } model.getAccessToken = function (bearerToken, callback) { console.log('in getAccessToken (bearerToken: ' + bearerToken + ')'); try { var decoded = jwt.verify(bearerToken, secretKey, { ignoreExpiration: true //OAuth2 サーバー実装によって処理済み }); callback(null, { accessToken: bearerToken, clientId: decoded.sub, userId: decoded.user, expires: new Date(decoded.exp * 1000) }); } catch(e) { callback(e); } }; model.saveAccessToken = function (token, clientId, expires, userId, callback) { console.log('in saveAccessToken (token: ' + token + ', clientId: ' + clientId + ', userId: ' + userId.id + ', expires: ' + expires + ')'); //JWT tokens トークンを保存する必要はありません。 console.log(jwt.decode(token, secretKey)); callback(null); }; OAuth2 トークンエンドポイント (/oauth/token) はあらゆるタイプのgrant(パスワードやRefresh Token)を発行・処理します。その他のエンドポイントはAccess Tokenをチェックする OAuth2 ミドルウェアによって保護されます。 // リクエストを許可するトークンを処理します app.all('/oauth/token', app.oauth.grant()); app.get('/secret', app.oauth.authorise(), function (req, res) { // 有効な access_token が必要です res.send('Secret area'); }); 例えば、パスワードに 'test' を設定したユーザー 'test' と、client secretに 'secret' を設定した 'testclient' というクライアントがある場合、以下のように、新しいAccess Token/Refresh Tokenのペアをリクエストできます。 $ curl -X POST -H 'Authorization: Basic dGVzdGNsaWVudDpzZWNyZXQ=' -d 'grant_type=password&username=test&password=test' localhost:3000/oauth/token { "token_type":"bearer", "access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiVlx1MDAxNcKbwoNUwoonbFPCu8KhwrYiLCJpYXQiOjE0NDQyNjI1NDMsImV4cCI6MTQ0NDI2MjU2M30.MldruS1PvZaRZIJR4legQaauQ3_DYKxxP2rFnD37Ip4", "expires_in":20, "refresh_token":"fdb8fdbecf1d03ce5e6125c067733c0d51de209c" } Authorization Headerには、client idとclient secretをBASE64 (testclient:secret) でエンコードした内容を含んでいます。 Access Tokenを使用して保護されたリソースにアクセスするには: $ curl 'localhost:3000/secret?access_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiVlx1MDAxNcKbwoNUwoonbFPCu8KhwrYiLCJpYXQiOjE0NDQyNjI1NDMsImV4cCI6MTQ0NDI2MjU2M30.MldruS1PvZaRZIJR4legQaauQ3_DYKxxP2rFnD37Ip4' Secret area "secret area" にアクセスしても、JWTのおかげでデータベース検索によってAccess Tokenが検証されることはありません。 トークンが期限切れになる��: $ curl 'localhost:3000/secret?access_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiVlx1MDAxNcKbwoNUwoonbFPCu8KhwrYiLCJpYXQiOjE0NDQyNjI2MTEsImV4cCI6MTQ0NDI2MjYzMX0.KkHI8KkF4nmi9z6rAQu9uffJjiJuNnsMg1DC3CnmEV0' { "code":401, "error":"invalid_token", "error_description":"The access token provided has expired." } これで、以下のようにトークンエンドポイントをヒットすることで、新しいAccess Tokenを取得するためにRefresh Tokenを使用することができます。 curl -X POST -H 'Authorization: Basic dGVzdGNsaWVudDpzZWNyZXQ=' -d 'refresh_token=fdb8fdbecf1d03ce5e6125c067733c0d51de209c&grant_type=refresh_token' localhost:3000/oauth/token { "token_type":"bearer", "access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiVlx1MDAxNcKbwoNUwoonbFPCu8KhwrYiLCJpYXQiOjE0NDQyNjI4NjYsImV4cCI6MTQ0NDI2Mjg4Nn0.Dww7TC-d0teDAgsmKHw7bhF2THNichsE6rVJq9xu_2s", "expires_in":20, "refresh_token":"7fd15938c823cf58e78019bea2af142f9449696a" } DISCLAIMER: 上記例で説明したコードは本番環境用ではありませんので、ご注意ください。. 完全なコードについては こちらからご覧ください。 補足 Auth0 アプリで Refresh Tokenを 使用する Auth0 はユーザーにとって重要な難しい認証サービスを提供しています。Refresh Tokenも例外ではありません。Auth0 を使って アプリをセットアップしたら、 こちらからドキュメントに従ってRefresh Tokenの取得方法を学びましょう。 まとめ Refresh Tokenはセキュリティを向上させ改善し、待機時間を削減し、Authorization Serverへのアクセスパターンを改善します。実装は JWT + JWS のようなツールを使って簡単にできます。トークン(そして、Cookie)についての詳細は こちら の記事をご覧ください。 詳細については、 Refresh Token ランディングページでもご確認いただけます。
0 notes
hackersandslackers · 7 years ago
Photo
Tumblr media
Extracting Massive Datasets in Python
Abusing APIs for all they’re worth
Taxation without representation. Colonialism. Not letting people eat cake. Human beings rightfully meet atrocities with action in an effort to change the worked for the better. Cruelty by mankind justifies revolution, and it is this writer's opinion that API limitations are one such cruelty. The data we need and crave is stashed in readily available APIs all around us. It's as though we have the keys to the world, but that power often cones with a few caveats: Your "key" only lasts a couple hours, and if you want another one, you'll have to use some other keys to get another key. You can have the ten thousand records you're looking for, but you can only pull 50 at a time. You won't know the exact structure of the data you're getting, but it'll probably be a JSON hierarchy designed by an 8 year old. All men may be created equal, but APIs are not. In the spirit of this 4th of July, let us declare independence from repetitive tasks: One Script, under Python, for Liberty and Justice for all. Project Setup We'll split our project up by separation of concern into just a few files: myProject ├── main.py ├── config.py └── token.py Main.py will unsurprisingly hold the core logic of our script. Config.py contains variables such as client secrets and endpoints which we can easily swap when applying this script to different APIs. For now we'll just keep variables client_id and client_secret in there for now. Token.py serves the purpose of Token Generation. Let's start there. That's the Token Since we're assuming worst case scenarios let's focus on atrocity number one: APIs which require expiring tokens. There are some tyrants in this world who believe that in order to use their API, it is necessary to to first use a client ID and client secret to generate a Token which quickly becomes useless hours later. In other words, you need to use an API every time you want to use the actual API. Fuck that. import requests from config import client_id, client_secret token_url = 'https://api.fakeapi.com/auth/oauth2/v2/token' def generateToken(): r = requests.post(token_url, auth=(client_id, client_secret), json={"grant_type": "client_credentials"}) bearer_token = r.json()['access_token'] print('new token = ', bearer_token) return bearer_token token = generateToken() We import client_id and client_secret from our config file off the bat: most services will grant these things simply by signing up for their API. Many APIs have an endpoint which specifically serves the purpose of acceppting these variables and spitting out a generated token. token_url is the variable we use to store this endpoint. Our token variable invokes our generateToken() function which stores the resulting Token. With this out of the way, we can now call this function every time we use the API, so we never have to worry about expiring tokens. Pandas to the Rescue We've established that we're looking to pull a large set of data, probably somewhere in the range of thousands of records. While JSON is all fine and dandy, it probably isn't very useful for human beings to consume a JSON file with thousands of records. Again, we have no idea what the nature of the data coming through will look like. I don't really care to manually map values to fields, and I'm guessing you don't either. Pandas can help us out here: by passing the first page of records to Pandas, we can generate the resulting keys into columns in a dataframe. It's almost like having a database-type schema created for you simply by looking at the data coming through: import requests import pandas as pd import numpy as np import json from token import token def setKeys(): headers = {"Authorization":"Bearer " + token} r = requests.get(base_url + 'users', headers=headers) dataframe = pd.DataFrame(columns=r.json()['data'][0].keys()) return dataframe records_df = setKeys() We can now store all data into records_df moving forward, allowing us to build a table of results. No Nation for Pagination And here we are, one of the most obnoxious parts of programming: paginated results. We want thousands of records, but we're only allowed 50 at a time. Joy. We've already set records_df earlier as a global variable, so we're going to append every page of results we get to that dataframe, starting at page #1. The function getRecords is going to pull that first page for us. base_url = 'https://api.fakeapi.com/api/1/' def getRecords(): headers = {"Authorization": "Bearer " + token} r = requests.get(base_url + 'users', headers=headers) nextpage = r.json()['pagination']['next_link'] records_df = pd.DataFrame(columns=r.json()['data'][0].keys()) if nextpage: getNextPage(nextpage) getRecords() Luckily APIs if there are additional pages of results to a request, most APIs will provide a URL to said page, usually stored in the response as a value. In our case, you can see we find this value after making the request: nextpage = r.json()['pagination']['next_link'] . If this value exists, we make a call to get the next page of results. page = 1 def getNextPage(nextpage): global page page = page + 1 print('PAGE ', page) headers = {"Authorization": "Bearer " + token} r = requests.get(nextpage, headers=headers) nextpage = r.json()['pagination']['next_link'] records = r.json()['data'] for user in records: s = pd.Series(user,index=user.keys()) global records_df records_df.loc[len(records_df)] = s records_df.to_csv('records.csv') if nextpage: getNextPage(nextpage) Our function getNextPage hits that next page of results, and appends them to the pandas dataframe we created earlier. If another page exists after that, the function runs again, and our page increments by 1. As long as more pages exist, this function will fire again and again until all innocent records are driven out of their comfortable native resting place and forced into our contained dataset. There's not much more American than that. There's More We Can Do This script is fine, but it can optimized to be even more modular to truly be one-size-fits-all. For instance, some APIs don't tell you the number of pages you should except, but rather the number of records. In those cases, we'd have to divide total number of records by records per page to know how many pages to expect. As much as I want to go into detail about writing loops on the 4th of July, I don't. At all. There are plenty more examples, but this should be enough to get us thinking how we can replace tedious work with machines. That sounds like a flavor that pairs perfectly with Bud Light and hotdogs if you ask me.
- Todd Birchard
0 notes
toddbirchard-architect · 7 years ago
Photo
Tumblr media
Extracting Massive Datasets in Python
Abusing APIs for all they’re worth
Taxation without representation. Colonialism. Not letting people eat cake. Human beings rightfully meet atrocities with action in an effort to change the worked for the better. Cruelty by mankind justifies revolution, and it is this writer's opinion that API limitations are one such cruelty. The data we need and crave is stashed in readily available APIs all around us. It's as though we have the keys to the world, but that power often cones with a few caveats: Your "key" only lasts a couple hours, and if you want another one, you'll have to use some other keys to get another key. You can have the ten thousand records you're looking for, but you can only pull 50 at a time. You won't know the exact structure of the data you're getting, but it'll probably be a JSON hierarchy designed by an 8 year old. All men may be created equal, but APIs are not. In the spirit of this 4th of July, let us declare independence from repetitive tasks: One Script, under Python, for Liberty and Justice for all. Project Setup We'll split our project up by separation of concern into just a few files: myProject ├── main.py ├── config.py └── token.py Main.py will unsurprisingly hold the core logic of our script. Config.py contains variables such as client secrets and endpoints which we can easily swap when applying this script to different APIs. For now we'll just keep variables client_id and client_secret in there for now. Token.py serves the purpose of Token Generation. Let's start there. That's the Token Since we're assuming worst case scenarios let's focus on atrocity number one: APIs which require expiring tokens. There are some tyrants in this world who believe that in order to use their API, it is necessary to to first use a client ID and client secret to generate a Token which quickly becomes useless hours later. In other words, you need to use an API every time you want to use the actual API. Fuck that. import requests from config import client_id, client_secret token_url = 'https://api.fakeapi.com/auth/oauth2/v2/token' def generateToken(): r = requests.post(token_url, auth=(client_id, client_secret), json={"grant_type": "client_credentials"}) bearer_token = r.json()['access_token'] print('new token = ', bearer_token) return bearer_token token = generateToken() We import client_id and client_secret from our config file off the bat: most services will grant these things simply by signing up for their API. Many APIs have an endpoint which specifically serves the purpose of acceppting these variables and spitting out a generated token. token_url is the variable we use to store this endpoint. Our token variable invokes our generateToken() function which stores the resulting Token. With this out of the way, we can now call this function every time we use the API, so we never have to worry about expiring tokens. Pandas to the Rescue We've established that we're looking to pull a large set of data, probably somewhere in the range of thousands of records. While JSON is all fine and dandy, it probably isn't very useful for human beings to consume a JSON file with thousands of records. Again, we have no idea what the nature of the data coming through will look like. I don't really care to manually map values to fields, and I'm guessing you don't either. Pandas can help us out here: by passing the first page of records to Pandas, we can generate the resulting keys into columns in a dataframe. It's almost like having a database-type schema created for you simply by looking at the data coming through: import requests import pandas as pd import numpy as np import json from token import token def setKeys(): headers = {"Authorization":"Bearer " + token} r = requests.get(base_url + 'users', headers=headers) dataframe = pd.DataFrame(columns=r.json()['data'][0].keys()) return dataframe records_df = setKeys() We can now store all data into records_df moving forward, allowing us to build a table of results. No Nation for Pagination And here we are, one of the most obnoxious parts of programming: paginated results. We want thousands of records, but we're only allowed 50 at a time. Joy. We've already set records_df earlier as a global variable, so we're going to append every page of results we get to that dataframe, starting at page #1. The function getRecords is going to pull that first page for us. base_url = 'https://api.fakeapi.com/api/1/' def getRecords(): headers = {"Authorization": "Bearer " + token} r = requests.get(base_url + 'users', headers=headers) nextpage = r.json()['pagination']['next_link'] records_df = pd.DataFrame(columns=r.json()['data'][0].keys()) if nextpage: getNextPage(nextpage) getRecords() Luckily APIs if there are additional pages of results to a request, most APIs will provide a URL to said page, usually stored in the response as a value. In our case, you can see we find this value after making the request: nextpage = r.json()['pagination']['next_link'] . If this value exists, we make a call to get the next page of results. page = 1 def getNextPage(nextpage): global page page = page + 1 print('PAGE ', page) headers = {"Authorization": "Bearer " + token} r = requests.get(nextpage, headers=headers) nextpage = r.json()['pagination']['next_link'] records = r.json()['data'] for user in records: s = pd.Series(user,index=user.keys()) global records_df records_df.loc[len(records_df)] = s records_df.to_csv('records.csv') if nextpage: getNextPage(nextpage) Our function getNextPage hits that next page of results, and appends them to the pandas dataframe we created earlier. If another page exists after that, the function runs again, and our page increments by 1. As long as more pages exist, this function will fire again and again until all innocent records are driven out of their comfortable native resting place and forced into our contained dataset. There's not much more American than that. There's More We Can Do This script is fine, but it can optimized to be even more modular to truly be one-size-fits-all. For instance, some APIs don't tell you the number of pages you should except, but rather the number of records. In those cases, we'd have to divide total number of records by records per page to know how many pages to expect. As much as I want to go into detail about writing loops on the 4th of July, I don't. At all. There are plenty more examples, but this should be enough to get us thinking how we can replace tedious work with machines. That sounds like a flavor that pairs perfectly with Bud Light and hotdogs if you ask me.
- Todd Birchard Read post
0 notes
hackersandslackers · 7 years ago
Photo
Tumblr media
Extracting Massive Datasets in Python
Abusing APIs for all they’re worth
Taxation without representation. Colonialism. Not letting people eat cake. Human beings rightfully meet atrocities with action in an effort to change the worked for the better. Cruelty by mankind justifies revolution, and it is this writer's opinion that API limitations are one such cruelty. The data we need and crave is stashed in readily available APIs all around us. It's as though we have the keys to the world, but that power often cones with a few caveats: Your "key" only lasts a couple hours, and if you want another one, you'll have to use some other keys to get another key. You can have the ten thousand records you're looking for, but you can only pull 50 at a time. You won't know the exact structure of the data you're getting, but it'll probably be a JSON hierarchy designed by an 8 year old. All men may be created equal, but APIs are not. In the spirit of this 4th of July, let us declare independence from repetitive tasks: One Script, under Python, for Liberty and Justice for all. Project Setup We'll split our project up by separation of concern into just a few files: myProject ├── main.py ├── config.py └── token.py Main.py will unsurprisingly hold the core logic of our script. Config.py contains variables such as client secrets and endpoints which we can easily swap when applying this script to different APIs. For now we'll just keep variables client_id and client_secret in there for now. Token.py serves the purpose of Token Generation. Let's start there. That's the Token Since we're assuming worst case scenarios let's focus on atrocity number one: APIs which require expiring tokens. There are some tyrants in this world who believe that in order to use their API, it is necessary to to first use a client ID and client secret to generate a Token which quickly becomes useless hours later. In other words, you need to use an API every time you want to use the actual API. Fuck that. import requests from config import client_id, client_secret token_url = 'https://api.fakeapi.com/auth/oauth2/v2/token' def generateToken(): r = requests.post(token_url, auth=(client_id, client_secret), json={"grant_type": "client_credentials"}) bearer_token = r.json()['access_token'] print('new token = ', bearer_token) return bearer_token token = generateToken() We import client_id and client_secret from our config file off the bat: most services will grant these things simply by signing up for their API. Many APIs have an endpoint which specifically serves the purpose of acceppting these variables and spitting out a generated token. token_url is the variable we use to store this endpoint. Our token variable invokes our generateToken() function which stores the resulting Token. With this out of the way, we can now call this function every time we use the API, so we never have to worry about expiring tokens. Pandas to the Rescue We've established that we're looking to pull a large set of data, probably somewhere in the range of thousands of records. While JSON is all fine and dandy, it probably isn't very useful for human beings to consume a JSON file with thousands of records. Again, we have no idea what the nature of the data coming through will look like. I don't really care to manually map values to fields, and I'm guessing you don't either. Pandas can help us out here: by passing the first page of records to Pandas, we can generate the resulting keys into columns in a dataframe. It's almost like having a database-type schema created for you simply by looking at the data coming through: import requests import pandas as pd import numpy as np import json from token import token def setKeys(): headers = {"Authorization":"Bearer " + token} r = requests.get(base_url + 'users', headers=headers) dataframe = pd.DataFrame(columns=r.json()['data'][0].keys()) return dataframe records_df = setKeys() We can now store all data into records_df moving forward, allowing us to build a table of results. No Nation for Pagination And here we are, one of the most obnoxious parts of programming: paginated results. We want thousands of records, but we're only allowed 50 at a time. Joy. We've already set records_df earlier as a global variable, so we're going to append every page of results we get to that dataframe, starting at page #1. The function getRecords is going to pull that first page for us. base_url = 'https://api.fakeapi.com/api/1/' def getRecords(): headers = {"Authorization": "Bearer " + token} r = requests.get(base_url + 'users', headers=headers) nextpage = r.json()['pagination']['next_link'] records_df = pd.DataFrame(columns=r.json()['data'][0].keys()) if nextpage: getNextPage(nextpage) getRecords() Luckily APIs if there are additional pages of results to a request, most APIs will provide a URL to said page, usually stored in the response as a value. In our case, you can see we find this value after making the request: nextpage = r.json()['pagination']['next_link'] . If this value exists, we make a call to get the next page of results. page = 1 def getNextPage(nextpage): global page page = page + 1 print('PAGE ', page) headers = {"Authorization": "Bearer " + token} r = requests.get(nextpage, headers=headers) nextpage = r.json()['pagination']['next_link'] records = r.json()['data'] for user in records: s = pd.Series(user,index=user.keys()) global records_df records_df.loc[len(records_df)] = s records_df.to_csv('records.csv') if nextpage: getNextPage(nextpage) Our function getNextPage hits that next page of results, and appends them to the pandas dataframe we created earlier. If another page exists after that, the function runs again, and our page increments by 1. As long as more pages exist, this function will fire again and again until all innocent records are driven out of their comfortable native resting place and forced into our contained dataset. There's not much more American than that. There's More We Can Do This script is fine, but it can optimized to be even more modular to truly be one-size-fits-all. For instance, some APIs don't tell you the number of pages you should except, but rather the number of records. In those cases, we'd have to divide total number of records by records per page to know how many pages to expect. As much as I want to go into detail about writing loops on the 4th of July, I don't. At all. There are plenty more examples, but this should be enough to get us thinking how we can replace tedious work with machines. That sounds like a flavor that pairs perfectly with Bud Light and hotdogs if you ask me.
- Todd Birchard
0 notes