# learning_react_ts **Repository Path**: acproject_admin/learning_react_ts ## Basic Information - **Project Name**: learning_react_ts - **Description**: 学习Typescript和React的笔记 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-01-11 - **Last Updated**: 2022-06-01 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 使用React Hooks 与 Typescript进行开发(自用笔记,不保证正确) ## 通过npx创建第一个react项目 ```shell # 第一步 npx create-react-app --template typescript trello-clone # 第二步 yarn && yarn build # 第三步, 查看效果 yarn start ``` ## 改造为EMP项目 (Option) [改造为EMP项目(可选项, 不想关注可以跳过的章节)]('docs/project_to_emp.md') ## 新建一个样式的ts文件 ``src/styles.ts`` 具体内容如下: ```ts import React from 'react'; import styled from 'styled-components'; // 采用react的方式来定义CSS export const buttonStyles :React.CSSProperties = { backgroundColor: "#5aac44", borderRadius: "3px", border: "none", boxShadow: "none" } // 下面采用styled-components定义 export const AppContainer = styled.div` align-items: flex-start; background-color: #3179ba; display: flex; flex-direction: row; height: 100%; padding: 20px; width: 100%; ` export const Button = styled.button` background-color: #5aac44; border-radius: 3px; border: none; box-shadow: none; ` export const ColumnContainer = styled.div` background-color: #ebecf0; width: 300px; min-height: 40px; margin-right: 20px; border-radius: 3px; padding: 8px 8px; flex-grow: 0; ` export const ColumnTitle = styled.div` padding: 6px 16px 12px; font-weight: bold; ` export const CardContainer = styled.div` background-color: #fff; cursor: pointer; margin-bottom: 0.5rem; padding: 0.5rem 1rem; max-width: 300px; border-radius: 3px; box-shadow: #091e4240 0px 1px 0px 0px; ` ``` 通过styled-components模块进行样式组建的编写需要下面的语句进行安装依赖: ```shell yarn add styled-components # 安装ts需要的对应的types yarn add @types/styled-components ``` ## 修改 ``src/App.tsx``文件 **原来的文件如下:** ```ts import React from 'react'; import logo from './logo.svg'; import './App.css'; function App() { return (
logo

Edit src/App.tsx and save to reload.

Learn React
); } export default App; ``` **修改为:** ```ts import React from 'react'; import logo from './logo.svg'; import './App.css'; import { AppContainer } from './styles' const App = () => { return ( 可以拖动的列 ); } export default App; ``` 通过 ``yarn start``测试修改后的节目 先看一下React的使用类组件的方式: ```ts import React from 'react'; interface CounterProps { message: string; } interface CounterState { count: number; } class Counter extends React.Component { state : CounterState = { count:0 }; render(): React.ReactNode { return (
{this.props.message} {this.state.count}
); } } export default Counter; ``` React.Component需要接收两个参数,一个props和state,如果你i定义的是函数组件就不需要这么麻烦,如下: ```ts export const Example = () => { return
函数组件
} ``` Typescript会自动推断出类型为JSX,如果你想明确是React函数组件,可以这样定义: ```ts export const Example:React.FC = () => { return
React函数组件
} ``` ## 串联所有组件的样式 需要的串联的组件有, 详细内容见 ``src/styles.ts``: * AppContainer * ColumnContainer * ColumnTitle * CardContaner ### 创建Column组件 1. 我们需要创建 ``src/Column.tsx``文件,并以函数组件方式定义,内容如下: ```tsx import React from "react"; export const Column = () => { return
列容器的标题
} ``` 2. 引入我们之前定义的style的组件 ```tsx import React from "react"; import {ColumnContainer, ColumnTitle} from './styles'; export const Column = () => { // return
列容器的标题
return ( 列容器的标题 ) } ``` 3. 如果想要给自定义函数组件添加props,我们需要这样进行操作 ```tsx import React from "react"; import {ColumnContainer, ColumnTitle} from './styles'; type ColumnProps = { text: string } export const Column = ({text}: ColumnProps) => { // return
列容器的标题
return ( 列容器的标题 ) } ``` 4. 如果我们有一个另外的组件是上面Column的组件的子组件,我们该如何定义呢?,其实也很简单,如下定义子组件: ```tsx import React from "react"; import {ColumnContainer, ColumnTitle} from './styles'; type ColumnProps = { text?: string | undefined } //children == type PropsWithChildren

= P & { children?: ReactNode | undefined }; export const Column = ({text, children}: React.PropsWithChildren) => { // return

列容器的标题
return ( {text} { /**这里children是由react负责去传递*/} {children} ) } ``` 5. 之前定义的body部分抽取出来定义一个新的组件为Card,内容如下: ```tsx import React from "react"; import {CardContainer} from './styles'; interface CardProps { text?: string| undefined } export const Card = ({text}: CardProps) => { return {text} } ``` 6. 我们将上面的内容串联起来,修改 ``src/App.tsx`` ```tsx import React from 'react'; import './App.css'; import { AppContainer } from './styles' import {Card} from './Card' import {Column} from './Column' const App = () => { return ( ); } export default App; ``` ## 准备新的组件,并通过react的hooks实现State与事件的集成 ### 编写新的自定义组件 修改 ``src/styles.ts``文件 ```ts interface AddItemButtonProps { dark?: boolean } export const AddItemButton = styled.button` background-color: #ffffff3d; border-radius: 3px; border: none; color: ${props = > (props.dark ? "#000" : "#fff")}; cursor: pointer; max-width: 300px; padding: 10px 12px; text-align: left; transition: background 85ms ease-in; width: 100%; &:hover { background-color: #ffffff52; } ` export const NewItemFormContainer = styled.div` max-width: 300px; display: flex; flex-direction: column; width: 100%; align-items: flex-start; ` export const NewItemButton = styled.button` background-color: #5aac44; border-radius: 3px; border: none; box-shadow: none; color: #fff; padding: 6px 12px; text-align: center; ` export const NewItemInput = styled.input` border-radius:3px; border: none; box-shadow: #091e4240 0px 1px 0px 0px; margin-bottom: 0.5rem; padding: 0.5rem 1rem; width: 100%; ` ``` ### 创建带有State的组件 ```tsx import React,{useState} from "react"; import { NewItemForm } from "./NewItemForm"; import { AddItemButton } from './styles'; interface AddNewItemProps { onAdd(text:string) :void toggleButtonText:string dark?: boolean | undefined } export const AddNewItem = (props: AddNewItemProps) => { const [showForm, setShowForm] = useState(false); const {onAdd, toggleButtonText, dark} = props; if (showForm) { return ( { onAdd(text) setShowForm(false) }} /> ) } return ( setShowForm(true)}> {toggleButtonText} ) } ``` 将组建添加到 ``src/Column.tsx``组件中去,代码如下: ```tsx ``` ### 创建自动聚焦的Input自定义组件,通过useRef这个hook来实现 定义自己的Hooks,在 ``src``文件夹中创建一个新的 ``utils``文件夹,并新建文件 ``useFocus.ts`` ```ts import {useRef, useEffect} from 'react'; // Refs提供了一种被React渲染的实际DOM节点的方法 export const useFocus = () => { const ref = useRef(null) useEffect(() => { ref.current?.focus() }) return ref; } ``` 回到之前的 ``NewItemForm.tsx``文件中,并引入上面的 ``useFocus.ts``, 修改部分如下: ```tsx const inputRef = useFocus(); return ( setText(e.target.value)} ref={inputRef} /> onAdd(text)}>创建新的表单 ) ``` ## 添加全局状态和业务逻辑 ### 使用useReducer这个hooks来管理复杂的状态 在 ``App.tsx``中创建Reducer的hooks,测试代码如下: ```tsx type State = {count:number} type Action = {type: "increment"} | {type: "decrement"} const App = () => { const counterReducer = (state:State, action:Action) => { switch(action.type) { case 'increment': return {count: state.count + 1} case 'decrement': return {count : state.count - 1} default: throw new Error() } } const [state, dispatch] = useReducer(counterReducer, {count: 0}) return ( <>

Count: {state.count}

) ``` ### 创建AppState的类型 通常React应用程序中,数据通过props自上而下(父到子)传递,但对于应用程序中许多组件需要在组件之间共享一些数据,而不必在每个层级显示传递一个prop,这时我们采用Context进行共享一个组件树的Prop。让我们在 ``src``文件夹内部创建 ``AppStateContext.tsx`` ```tsx import React, {createContext} from "react"; type Task = { id:string text:string } type List = { id: string text: string tasks: Task[] } export type AppState = { lists: List[] } type AppStateContextProps = { state: AppState } const AppStateContext = createContext({} as AppStateContextProps) export const AppStateProvider = ({children}: React.PropsWithChildren<{}>) => { return ( {children} ) } const appData: AppState = { lists: [ {id:"0", text:"To Do", tasks: [{id: "c0", text: "去买一杯咖啡"}]}, {id:"1", text:"修改Bug", tasks: [{id: "c2", text: "需要修改React中出现的问题"}]}, {id:"2", text:"已经编写完成", tasks: [{id: "c3", text: "工作计划已将编写完成"}]} ] } ``` ### 用AppStateProvier包装原有App组件 在 ``src``文件夹中创建 ``index.tsx``,内容如下: ```tsx import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import { AppStateProvider } from './AppStateContext' ReactDOM.render( , document.getElementById('root') ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals(); ``` 继续修改 ``AppStateContext.tsx``,使用全局的上下文数据 ```tsx import React, {createContext, useReducer, useContext} from "react"; export const useAppState = () => { return useContext(AppStateContext) } ``` 修改 ``App.tsx``,替换已经常量为 ``AppStateContext.tsx``的数据,并修改 ``Column.tsx`` ```tsx // AppStateContext.tsx import { useAppState } from './AppStateContext'; const { state } = useAppState() return ( {state.lists.map((list, i) => ( ))}

Count: {state2.count}

) ``` ```tsx // Column.tsx import { AddNewItem } from "./AddNewItem"; import { useAppState } from "./AppStateContext"; import { Card } from "./Card"; import {ColumnContainer, ColumnTitle} from './styles'; type ColumnProps = { text?: string | undefined index: number } export const Column = ({text,index}: ColumnProps) => { // return
列容器的标题
const {state} = useAppState() return ( {text} {state.lists[index].tasks.map(task => ( ))} ) } ``` ### 定义Action和Reducer 修改 ``AppStateContext.tsx``文件,并修改 ``AppStateContextProps``的类型定义 ```tsx // 定义Action type Action = { type: "ADD_LIST", payload: string, } | { type: "ADD_TASK", payload: { text: string; taskId: string } } | { type: "MOVE_LIST", payload: { dragIndex: number hoverIndex: number } } type AppStateContextProps = { state: AppState, dispatch: React.Dispatch // new // 手工定义假数据 const appData: AppState = { lists: [ { id: "0", text: "To Do", tasks: [{ id: "c0", text: "去买一杯咖啡" }] }, { id: "1", text: "修改Bug", tasks: [{ id: "c2", text: "需要修改React中出现的问题" }] }, { id: "2", text: "已经编写完成", tasks: [{ id: "c3", text: "工作计划已将编写完成" }] } ] } /** * 用Reducer代替,上面定义的AppState,让组件按照Action来进行调用 */ const appStateReducer = (state: AppState, action: Action): AppState => { switch (action.type) { case "ADD_LIST": { const visibilityExample = "可见的" return { ...state, lists: [ ...state.lists, { id: uuidv4(), text: action.payload, tasks: [] } ] } } case "ADD_TASK": { const visibilityExample = "可见的" return { ...state } } case "MOVE_LIST": { } default: { return state } } } export const AppStateProvider = ({ children }: React.PropsWithChildren<{}>) => { const [state, dispatch] = useReducer(appStateReducer, appData) return ( {children} ) } ``` ## 添加可以拖动的组件库 ```shell yarn add react-dnd react-dnd-html5-backend ``` ### 创建Dnd提供器 修改``index.tsx``文件,增加Dnd组件 ```tsx import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import { AppStateProvider } from './AppStateContext' import { DndProvider, } from 'react-dnd'; import { HTML5Backend as Backend } from 'react-dnd-html5-backend' ReactDOM.render( , document.getElementById('root') ); ``` ### 通过Action存储拖后的数据保存到State中 要实现这个功能,要添加Action,所以要修改``AppStateContext.tsx``文件中的Action部分 **注意:** react-dnd的hooks只能回调当前拖动的项目数据,如果我们创建一个拖动预览,看起来就像是在复制一个组件,为了修复这个问题我们需要隐藏当前的组件,所有我们需要知道我们拖动的是什么内容(组件),它到底是卡片还是列。我能需要通过ID来确定。这里我们将使用``useDrag``和``useDrop``两个hooks来完成组件的拖放操作,创建一个新的文件``ColumnDragItem.ts``来实现拖的动作 ```ts export interface ColumnDragItem { index: number id: string text: string type: "COLUMN" } export type DragItem = ColumnDragItem; ``` ```tsx import { DragItem } from "./ColumnDragItem"; type Action = | { type: "SET_DRAGGED_ITEM" payload: DragItem | undefined } ``` ### 定义``useItemDrag``Hook 这里可以新建一个自定义的Hook,它将返回一个可以接受拖动元素的拖动方法,每当开始拖动的时候都会发送SET_DRAG_ITEM操作,当停止拖动时,将在未定义的有效payload的情况下再次发送此操作。这里创建一个新文件``useItemDrag.ts`` ```ts import { useDrag } from 'react-dnd'; import { useAppState } from './AppStateContext'; import { DragItem } from './DragItem'; export const useItemDrag = (item: DragItem) => { const { dispatch } = useAppState() const [, drag] = useDrag({ type: "SET_DRAGGED_ITEM", item, end: () => dispatch({ type: "SET_DRAGGED_ITEM", payload: undefined }), }) return { drag } } ``` **注意:**新版本的``react-dnd``是没有begin属性的 ### 将被拖动的组件进行隐藏 ```tsx ```