# 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 (
{
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
```