今天的目標是把前端的步驟 1 、2、6、7 串聯起來。
分析來自 UI 的未知事件,在我們的應用上具有什麼商業意義
const parseEvent = (event: unknown): 'Nothing' | 'AddUserEvent' =>
pipe(
Match.value(event),
Match.when({ key: Match.string }, ({ key }) =>
pipe(
Match.value(key),
Match.when('Enter', () => 'AddUserEvent' as const),
Match.orElse(() => 'Nothing' as const)
)
),
Match.orElse(() => 'AddUserEvent' as const)
)
送出請求,把可能發生的未知的錯誤轉換成 RequestError
const getUser = (name: string): Effect.Effect<never, RequestError, any> =>
Effect.tryPromise({
try: () => axios.get(`/api/v1/users/${name}`).then((res) => res.data),
catch: (error) => requestErrorOf(error),
})
axios 提供了一個
isAxiosError
函式,可以用來幫助我們確認型別。例如以下範例,我們可以根據與後端資料提供服務約定好的格式,來把不同的錯誤代碼轉換成具有意義的錯誤型別。
export type RequestError =
| UnexpectedRequestError
| NotFoundError
| UnexpectedAxiosError
export const requestErrorOf = (error: unknown): RequestError =>
isAxiosError(error)
? pipe(
Match.value(error.response?.status),
Match.when(404, () => notFoundErrorOf(error)),
// Match.when(400, 401, 403, 500
Match.orElse(() => unexpectedRequestErrorOf(error))
)
: unexpectedRequestErrorOf(error)
在上一步驟, getUser
的回傳值是 any
,這非常危險,所以我們要用講了好幾天的 schema
來解析它。
class Administrator extends S.Class<Administrator>()({
_tag: S.literal('Administrator'),
name: S.Date,
}) {}
class Participant extends S.Class<Participant>()({
_tag: S.literal('Participant'),
name: S.string,
}) {}
const userSchema = S.union(Administrator, Participant)
export const validateUser: (
props: unknown
) => Either.Either<ValidateError, User> = flow(
S.parseEither(userSchema),
// 這邊 ValidateError.of 只是把原生的 ParseError 再包裝一層,有興趣可看原始碼
Either.mapLeft(ValidateError.of)
)
不過 validateUser
是 Either
, getUser
的輸出是 Effect
,會不會有不能合在一起的問題呢? 其實不用擔心,我們可以把 Either
當作一種特殊的 Effect
,直接 flatMap
串起來就好囉 !
type GetUserError = ValidateError | RequestError
const getUser = (name: string): Effect.Effect<never, GetUserError, User> =>
pipe(
Effect.tryPromise({
try: () => axios.get(`/api/v1/users/${name}`).then((res) => res.data),
catch: (error) => requestErrorOf(error),
}),
Effect.flatMap(validateUser)
)
藉由以上一連串操作,新版本的 getUser
函式加上驗證,就從 any
變成了 User
。此外錯誤型別也擴展了,變成 GetUserError
。
接下來是今天的重頭戲,回歸新增使用者
的異步流程,我們要思考我們要放回 Atom
的 狀態
是甚麼 ? 使用者按下 Enter
或是 Add
後畫面會如何發生改變 ? 我們如何測試 ? 如何確保變更如我們預期 ?
以下概念很適合用來概括前端框架的畫面變更模式
type update = (event:Event) => (stateBefore:Sate) => (stateAfter:State)
把以上概念轉換成新增使用者
這個需求,實作出來就會變成這樣
const onAddUserEvent = (event: 'AddUserEvent') => (field: UsersField) =>
pipe(
getUser(field.input),
Effect.match({
onFailure: (error) => UsersField.updateError(error)(field),
onSuccess: (user) => UsersField.addUser(user)(field),
})
)
// UsersField.on
const on = (event: 'AddUserEvent' | 'Nothing') => (field: UsersField) =>
pipe(
Match.value(event),
Match.when('Nothing', () => Effect.succeed(field)),
Match.when('AddUserEvent', (event) => onAddUserEvent(event)(field)),
Match.exhaustive
)
對我們來說 UsersField
變成了我們可以掌控的 狀態機
。現在它可以根據收到的 事件
來決定該如何做 狀態
的轉換,讓複雜的流程在這樣的設計下,變得簡單而透明。