validate-state-hook with fp-ts

Introduction

In this article, We will implement a react validate hook that is used in form-item by fp-ts step by step.

Why using the fp-ts?

First, it's supposed to help us to define and resolve the issue about type safety.

Second, actually, it inherits the thoughts of functional programming, helps us to simplify and process complex logic, that is gonnar be readable, improvement in code quality and easy to maintain.

Historical Article

Api Involved

Option

typescript 复制代码
type None = { _tag: 'None' };
type Some<A> = { _tag: 'Some', value: A};
type Option<A> = None | Some<A>
typescript 复制代码
import { Option, some, none, fromNullable, fromPredicate } from 'fp-ts/Option';

function findIndex<A>(arr: Array<A>, predicate: (a: A) => boolean): Option<number> {
	const index = arr.findIndex(predicate);
	return index === -1 ? none : some(index);
}

const arr = [1, 2, 3];
findIndex(arr, n => n === 2); // { _tag: 'Some', value: 1 }
findIndex(arr, n => n === 4); // { _tag: 'None' }

fromNullable(undefined); // { _tag: 'None' }
fromNullable(null); // { _tag: 'None' }
fromNullable(0); // { _tag: 'Some', value: 0 }

const isNumber = <T>(a: T) => !isNaN(Number(a));
const getOptionNumber = fromPredicate(isNumber);
getOptionNumber('a'); // {_tag: 'None'}
getOptionNumber('10'); // { _tag: 'Some', value: '10' }
getOptionNumber(1); // { _tag: 'Some', value: 1 }

pipe

typescript 复制代码
// 1 --- add1 ---> 2 ---- add2 ---> 4 --- add3 ---> 7
const add = (a: number) => (b: number) => a + b;
const add1 = add(1);
const add2 = add(2);
const add3 = add(3);
add3(add2(add1(1))); // 7
typescript 复制代码
// pipe(param, func1, [...func]) ===> func2(func1(1))
// param as the parameter of func1, func1(1) as the paramter of func2, and so on ...
import { pipe } from 'fp-ts/function';

const add = (a: number) => (b: number) => a + b;
const add1 = add(1);
const add2 = add(2);
const add3 = add(3);
pipe(1, add1, add2, add3); // add3(add2(add1(1)))
pipe(1, add1, add2, add3, add3, add3, add3, add3);
// add3(add3(add3(add3(add3(add2(add1(1)))))))

Either

typescript 复制代码
// Either<E, A> => Either functor
//  Either functor has two value: Left and Right,
//  Left: the default value when Right does not exsit,
//  Right:  the normal value
type Left<E> = { _tag: 'Left', left: E };
type Right<A> = { _tag: 'Right', right: A};
type Either<E, A> = Left<E> | Right<A>
typescript 复制代码
import { Either, tryCatch, fromNullable, fromPredicate } from 'fp-ts/Either';

function parse(s: string): Either<Error, unknown> {
  return tryCatch(
     () => JSON.parse(s),
     reason => new Error(String(reason))
  );
}

const success = '{"a": 1, "b": 2}';
const fail = '{"a": a, "b"}';

parse(success); // { _tag: 'Right', right: { a: 1, b: 2 } }
parse(fail); // { _tag: 'Left', left: Error:xxxxx

/******** fromNullable **********/
const getEitherString = fromNullable('defaultValue');
getEitherString(undefined); // { _tag: 'Left', left: 'defaultValue' }
getEitherString(null); // { _tag: 'Left', left: 'defaultValue' }
getEitherString('value'); // { _tag: 'Right', right: 'value' }

/******** fromPredicate **********/
const isEmptyString = (s: string) => s === '';
// fromPredicate(s => boolean, () => defaultValue); // if s => boolean is true, return s, otherwise, return defualtValue
const getEitherString2 = fromPredicate(
	(s: string) => !isEmptyString(s),
	() => 'defaultValue'
);
getEitherString2(''); // { _tag: 'Left', left: 'defaultValue' }
getEitherString2('abc'); // { _tag: 'Right', right: 'abc' }

match

typescript 复制代码
declare const optionNatch: <A, B>(onNone: () => B, onSome: (a: A) => B) => (ma:Option<A>) => B
declare const eitherNatch: <E, A, B>(onNone: (e: E) => B, onSome: (a: A) => B) => (ma: Either<E, A>) => B
typescript 复制代码
/***********************/
// match fold
// declare const optionNatch: <A, B>(onNone: () => B, onSome: (a: A) => B) => (ma: Option<A>) => B
// declare const eitherNatch: <E, A, B>(onNone: (e: E) => B, onSome: (a: A) => B) => (ma: Either<E, A>) => B
/***********************/
import { fromPredicate, match } from 'fp-ts/Option';
import { pipe } from 'fp-ts/function';

pipe(
  2,
  fromPredicate(value => value !== 0), // Option<number> ==> { _tag: 'Some', value: 2 }
  match(
    () => 0, // onNone callback
    value => 10 / value // onSome callback
  )
); // 5

// if the first parmeter is 0, the final result is 0

Chain

typescript 复制代码
declare const chain: <E, A, B>(f: (a: A) => Either<E, B>) => (ma: Either<E, A>) => Either<E, B>
typescript 复制代码
import { Either, chain, left, right } from 'fp-ts/Either';
import { pipe } from 'fp-ts/function';

const multiplyByTen = <T>(value: T): Either<string, number> =>
	typeof value === 'number' ? right(value * 10) : left('not a number');
        
const increment = (value: number): Either<string, number> => right(value + 1);

const func = <T>(value: T) => pipe(value, multiplyByTen, chain(increment));

Let's look at a react componnet demo

That is a component about phoneNumber validate, we generally code like this...

typescript 复制代码
import { useState, ChangeEvent, FormEvent } from 'react';

const FormTest = () => {
   const [mobleNumber, setMobileNumber] = useState('');
   const [error, setError] = useState('');

   const validateMobileNumber = (value: string): boolean => {
      if (!value) {
        setError('moblie number can not be empty');
        return false;
      }
      if (!/^1[3-9]\d{9}$/.test(value)) {
        setError('moble number does not follow the rule');
        return false;
      }

      setError('');
      return true;
   };

   const anotherValidate = (value: string) => {
      return true;
   };

   const handleNumberChange = (e: ChangeEvent<HTMLInputElement>) => {
      const { value } = e.target;
      validateMobileNumber(value);
      setMobileNumber(value);
   };

   const onSubmit = (e: FormEvent) => {
      e.preventDefault();
      const validations =
        [validateMobileNumber(mobleNumber), anotherValidate(mobleNumber)];
      
      if (validations.some(valid => !valid)) {
	      return;
      }
      alert('submit...');
   };

   return (
     <form onSubmit={onSubmit}>
         <input
            type="text"
            onChange={handleNumberChange}
            value={mobleNumber}
         />
         <span style={{ color: 'red' }}>{error}</span>
         <div>
             <button type="submit">submit</button>
         </div>
     </form>
   );
};

export default FormTest;

Start from fp-ts

1. validate rules

typescript 复制代码
import type { Predicate } from 'fp-ts/Predicate';
// interface Predicate<A> {  
//    (a: A): boolean  
// }

// function currying

const startsWith =
	(search: string): Predicate<string> =>
	(text: string) =>
	   text.startsWith(search);

const minLength =
	(limit: number): Predicate<string> =>
	(text: string) =>
	  text.length >= limit;

const maxLength =
	(limit: number): Predicate<string> =>
	(text: string) =>
	  text.length <= limit;

const testPhoneNumberPattern = (text: string) => !/[^0-9]/gi.test(text);

// test
const myMobileNum = '01012345';
testPhoneNumberPattern(myMobileNum); // true
testPhoneNumberPattern('0101a2345'); // false
startsWith('01')(myMobileNum); // true
maxLength(11)(myMobileNum); // true
minLength(10)(myMobileNum); // false

2. validate function

typescript 复制代码
import type { Predicate } from 'fp-ts/Predicate';
import { Either, chain, fromPredicate } from 'fp-ts/Either';
import { pipe } from 'fp-ts/function';
import { every, map } from 'fp-ts/Array';

// ... validate rules code

const validate =
	<T>(validators: Array<Predicate<T>>, errorMessage: string) =>
	(value: T) =>
	  pipe(
          value,
          fromPredicate(
          val => 
              pipe(
                  validators,
                  map(fn => fn(val)),
                  every(Boolean)
              ),
              () => errorMessage
         )
     );
     
export const validatePhoneNumber = (phoneNumber: string): Either<string, string> =>
	pipe(
        phoneNumber,
        validate([minLength(1)], 'mobile number can not be empty'),
        chain(
          validate(
            [testPhoneNumberPattern, startsWith('01'), minLength(10), maxLength(11)],
            'mobile number must confirm the rule'
          )
        )
   );

// test
validatePhoneNumber(''); // Left('moblie number can not be empty')
validatePhoneNumber('0123abc'); // Left('moble number must confirm the rule')
validatePhoneNumber('01011234567'); // Right('01011234567')

3.hook - useStateWithValidator.ts

It replaces the feature of useState, involving validate and error-show functionality

typescript 复制代码
import { useState } from 'react';
import { Either, match } from 'fp-ts/Either';
import { empty, isEmpty } from 'fp-ts/string';
import { identity, pipe } from 'fp-ts/function';

type StateValidator = {
	validate: () => boolean;
	error: string;
};

/**
 * hook
 * @param initialState 
 * @param validator 
 * @returns [value, changeValue, error, StateValidator]
 */
const useStateWithValidator = <T>(
	initialState: T,
	validator: (v: T) => Either<string, T>
): [T, (v: T) => void, string, StateValidator] => {
	const [value, setValue] = useState<T>(initialState);
	const [error, setError] = useState('');

	const changeError = (errorMessage: string): string => {
		setError(errorMessage);
		return errorMessage;
	};

        /**
         * match(onLeft, onRight)
         * function identity<A>(a: A): A ==> input: A,output: A
         */
	const changeValue = (v: T): void => {
		pipe(
			v, // input value as the param of setValue
			setValue, // setState
			() => validator(v), // validate value
			match(identity, () => empty), // if the result of validate is left, invalid, then trigger identity, otherwise, it was passed,trigger () => empty
			changeError // modify error. 
		);
	};

	const stateValidator: StateValidator = {
		validate(): boolean {
			return pipe(
				validator(value),
				match(identity, () => empty),
				changeError,
				isEmpty
			);
		},
		get error(): string {
			return error;
		},
	};

	return [value, changeValue, error, stateValidator];
};

export default useStateWithValidator;

4. use hook in component

typescript 复制代码
import { ChangeEvent, FormEvent } from 'react';
import useStateWithValidator from '../../hooks/useStateWithValidator';
import { validatePhoneNumber } from '../utils';

export default () => {
    const [mobileNumber, setMobileNumber, error, mobileNumberValidator] =
        useStateWithValidator('', validatePhoneNumber);
	
    const handleNumberChange = (e: ChangeEvent<HTMLInputElement>) =>
        setMobileNumber(e.target.value);

    const onSubmit = (e: FormEvent) => {
        e.preventDefault();
        const validators = [mobileNumberValidator];
        const isInvalid = validators.map(validator =>
            validator.validate()).some(valid => !valid);

        if (isInvalid) 
            return;
        }
        alert('submit...');
    };

    return (
        <form onSubmit={onSubmit}>
            <input
                type="text"
                onChange={handleNumberChange}
                value={mobileNumber}
            />
            <span style={{ color: 'red' }}>{error}</span>
            <div>
                <button type="submit">submit</button>
            </div>
        </form>
    );
};

Final

We implement a react hook with fp-ts, the primacy is not the hook, but the validate function. It works like magic, looks like complexer than our coding previously.

But more safer function logic and compose the validate function is a better way for programming. At same time, FP lets us organize the logic flow flexiblely, like this:

相关推荐
码海扬帆:前端探索之旅1 天前
深度定制 uni-combox:新增功能详解与实战指南
前端·vue.js·uni-app
谷雨不太卷1 天前
进程的状态码
java·前端·算法
打小就很皮...1 天前
基于 Python + LangChain + RAG 的知识检索系统实战
前端·langchain·embedding·rag
BJ-Giser1 天前
Cesium 烟雾粒子特效
前端·可视化·cesium
空中海1 天前
02 ArkTS 语言与工程规范
java·前端·spring
YJlio1 天前
7.4.5 Windows 11 企业网络连接与网络重置实战:远程访问、本地策略与故障恢复
前端·chrome·windows·python·edge·机器人·django
Slow菜鸟1 天前
Codex CLI 教程(五)| Skills 安装指南:面向 Java 全栈工程师打造个人 ECC(V1版)
大数据·前端·人工智能
Lee川1 天前
打字机是怎么炼成的:Chat 流式输出深度解析
前端·后端·面试
前端若水1 天前
过渡(transition)高级:贝塞尔曲线、硬件加速
前端·css·css3
Lee川1 天前
Token 无感刷新与 Logout:前端安全会话管理实战
前端·后端·react.js