Skip to main content

vite-react-extension

· 預估閱讀時間: 13 分鐘

繼上一次vite-react範本設定

這次要來擴充這個範本

作成我工作上習慣的配置環境

extension

  • sass / tailwindcss 3 / postcss
  • rxjs / observable-hooks
  • redux-toolkit
  • redux-observable / axios-observable

首先拉上一次建好的範本再另外做一個擴充版本的範本

$ npx degit sayaku/reactSample viteReactTemplate

下載後進到資料夾

$ cd viteReactTemplate

install sass / tailwindcss 3 / postcss

首先安裝跟tailwindcss相關的依賴

$ yarn add sass tailwindcss postcss autoprefixer --dev

裝完後初始化tailwindcss與postcss的設定檔

$ npx tailwindcss init -p

這時目錄下會多兩個設定檔postcss.config.cjs, tailwindcss.config.cjs出來

兩個檔案需要變動如下

module.exports= {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

vite預設會去找有沒有符合postcss-load-config的檔案並且去套用它

符合postcss-load-config檔名與副檔名可參考:這裡

postcss的config裡關於tailwindcss如果沒特別設定,預設會去抓tailwindcss.config

也就是我們把vite給run起來的時候就會一起去跑postcss與tailwindcss的設定了

vite設定真的少很多(望向webpack還要寫一堆loader...)

預處理css的部分

vite預設有支援sass, 不用另外像webpack來裝sass的loader, 但vite要另外裝sass的依賴(所以我們上面先裝過sass了)

就算你不用sass, vite也預設支援postcss-importpostcss-nesting

讓你可以在css上import其他css以及在css裡面像sass那樣能寫巢狀css

這邊我們先去引入基本的tailwindcss

然後在App.tsx做一些class的修改(套用tailwindcss class)

@tailwind base;
@tailwind components;
@tailwind utilities;

接下來正式跑一下

$ yarn run dev

跑起來發現我們的tailwindcss都套用上去了

到這邊基本的style的部分都設定好了

rxjs / observable-hooks

雖然簡單的非同步用Promise/async/await即可達成

但因為我原本使用Angular, 用習慣rxjs

不用rx來寫整個渾身不對勁

所以打算直接在react上使用rx

不得不說剛接觸react-hooks時還一時不知怎麼在hook上使用rx

直到使用observable-hooks後才了解原來rx是這樣寫hooks的

而後來發現react自己本身的hooks有些缺陷可以透過rx來解決

舉個例子好了,在useEffect裡的deps發生變動會觸發useEffect callback

但useEffect callback裡面你會知道是哪個deps發生改變嗎?(還是能做到但是沒那麼直覺)

這個例子使用observable-hooks就方便許多

所以我在使用react-hooks的原則就是能用原生hooks就使用原生的

比較複雜的狀態控制才會使用observable-hooks

接下來就開始裝observable-hooks rxjs

$yarn add observable-hooks rxjs

如何使用請參考官網範例(懂的就知道怎麼用了)

redux-toolkit

接下來就是做全域狀態管理

我們都熟悉使用redux, 但寫起來實在是很麻煩

所以我們用redux-toolkit來簡化寫redux的流程

首先安裝

$yarn add @reduxjs/toolkit react-redux

建立slice, 這裡面同時包含init state, action, reducer

專案上是否要把action, reducer或是epic分檔案寫可由團隊決定

新增資料夾與檔案如下

import { createAction, createSlice, PayloadAction } from "@reduxjs/toolkit";

const initialState: { count: number; name: string } = {
count: 0,
name: "sayaku",
};

// Actions
export const increment = createAction<number>("todo/INCREMENT");
export const decrement = createAction<number>("todo/DECREMENT");

/**
* Slice
*/
export const todoSlice = createSlice({
name: "todo",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(increment, (state, action: PayloadAction<number>) => {
return {
...state,
count: state.count + action.payload,
};
})
.addCase(decrement, (state, action: PayloadAction<number>) => {
return {
...state,
count: state.count - action.payload,
};
});
},
});


/**
* Reducer
*/
export default todoSlice.reducer;



基礎設定好以後

可以開始使用了

下面簡單使用如何從store拿值

以及透過Observable-hooks實現在hooks裡使用rx與狀態管理

App.tsx
import {
useObservable,
useObservableState,
} from "observable-hooks";
import { useDispatch, useSelector } from "react-redux";
import { map } from "rxjs";
import reactLogo from "./assets/react.svg";
import { decrement, increment } from "./store/slices/todo.slice";
import { RootState } from "./store/store.config";

function App() {
const count = useSelector((state: RootState) => state.todoStore.count);
const dispatch = useDispatch();
// 當store的count變動時透過rx去對count做處理,這邊的範例是當count變動時將count乘以2
const twiceCount = useObservableState(
useObservable((obs$) => obs$.pipe(map(([v]) => v * 2)), [count])
);

return (
<div className="flex flex-col">
<div className="w-full flex justify-around p-10">
<a href="https://vitejs.dev" target="_blank" className="basis-1/4">
<img
src="/vite.svg"
className="w-full aspect-square"
alt="Vite logo"
/>
</a>
<a href="https://reactjs.org" target="_blank" className="basis-1/4">
<img
src={reactLogo}
className="w-full aspect-square"
alt="React logo"
/>
</a>
</div>
<h1 className="self-center text-lg mx-5">Vite + React + Tailwindcss</h1>
<div className="w-full flex justify-around p-10">
<div>count: {count}</div>
<div>twiceCount: {twiceCount}</div>
</div>
<div className="w-full flex justify-around p-10">
<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" onClick={() => dispatch(increment(1))}>increment</button>
<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" onClick={() => dispatch(decrement(1))}>decrement</button>
</div>
</div>
);
}

export default App;

run起來後應該會長這樣, 可以正常運作

redux-observable / axios-observable

現在專案裡的Redux都是做同步處理

如果今天要加入打API等非同步處理雖然可以透過redux-toolkit裡的createAsyncThunk

但我們既然都使用rx了, 就統一都使用rx來串接非同步這塊

畢竟非同步就是rx的強項

當然你也可以在createAsyncThunk裡面使用rx

但我實在不喜歡使用rx還要在那邊轉成promise還有async/await

所以這邊透過Redux Middleware來處理非同步這一塊

而Middleware我們使用redux-observable來處理非同步

首先安裝redux-observable

$yarn add redux-observable -D

之後新增與修改下列檔案

import { createAction, createSlice, PayloadAction } from "@reduxjs/toolkit";
import { filter, map, mergeMap } from "rxjs";
import { AppEpic } from "../store.config";

const initialState: { count: number; name: string } = {
count: 0,
name: "sayaku",
};

// Actions
export const increment = createAction<number>("todo/INCREMENT");
export const decrement = createAction<number>("todo/DECREMENT");
export const updateName = createAction<string>("todo/UPDATE_NAME");

// Async Actions
export const fetchData = createAction<undefined>("todo/FETCH_DATA");

// Epics
export const fetchUserEpic: AppEpic = (action$, store, { getUserName }) =>
action$.pipe(
filter(fetchData.match),
mergeMap((action) =>
getUserName().pipe(map((response) => updateName(response.name)))
)
);

/**
* Slice
*/
export const todoSlice = createSlice({
name: "todo",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(increment, (state, action: PayloadAction<number>) => {
return {
...state,
count: state.count + action.payload,
};
})
.addCase(decrement, (state, action: PayloadAction<number>) => {
return {
...state,
count: state.count - action.payload,
};
})
.addCase(updateName, (state, action: PayloadAction<string>) => {
return {
...state,
name: action.payload,
};
});
},
});

/**
* Reducer
*/
export default todoSlice.reducer;

在Redux裡面的Middleware執行點是Dispatch Action後與進到Reducer之前

所以非同步的動作我們都會在Middleware裡面實作

我們這邊非同步使用redux-observable裡的Epic

這個如果在Angular上使用的ngrx裡是相當於effect的角色

特點在於Action in Action out

他的生命週期大概是這樣

回到App.tsx這邊做修改

import { useObservable, useObservableState } from "observable-hooks";
import { useDispatch, useSelector } from "react-redux";
import { map } from "rxjs";
import reactLogo from "./assets/react.svg";
import { decrement, fetchData, increment } from "./store/slices/todo.slice";
import { RootState } from "./store/store.config";

function App() {
const count = useSelector((state: RootState) => state.todoStore.count);
const name = useSelector((state: RootState) => state.todoStore.name);
const dispatch = useDispatch();
const twiceCount = useObservableState(
useObservable((obs$) => obs$.pipe(map(([v]) => v * 2)), [count])
);

return (
<div className="flex flex-col">
...
<div className="w-full flex justify-around p-10">
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
onClick={() => dispatch(fetchData())}
>
launch api
</button>

<div>name: {name}</div>
</div>
</div>
);
}

export default App;

這邊就完成redux裡打Api等非同步處理的動作了

在畫面上按下launch api, 兩秒後會更新store, 預期會看到name從sayaku換成monica

這邊我們的api是做假的

而實際上真實要打Api我會另外使用axios

axios-observable則是使用rxjs將axios包裝起來

同時也能做攔截器

是個很方便的套件

一樣也是把它裝起來

$yarn add axios axios-observable

簡單的操作GET/POST如下

GET
import Axios from  'axios-observable';
// or const Axios = require('axios-observable').Axios;

// Make a request for a user with a given ID
Axios.get('/user?ID=12345')
.subscribe(
response => console.log(response),
error => console.log(error)
);
POST
Axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.subscribe(
response => console.log(response),
error => console.log(error)
);

為什麼使用rx的observable會優於promise

比如一個情境

我在打API失敗後要retry, retry次數3次

你可以想想在promise的寫法會多麻煩

用observable的寫法就會直覺很多

Axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.pipe(
retry(3)
)
.subscribe(
response => console.log(response),
error => console.log(error)
);

相關功能與api很多可以參閱repo

以上就是我工作上基本上會用到的配置

像是還有其他code管理例如husky

或是關於Testing的部分之後再另開篇幅來新增

目前的code也直接變成一個新專案的範本

以後直接這樣下就可以了

$ npx degit sayaku/viteReactTemplate new_react_project