返回

react 笔记

目录

TS 速查

显示声明

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
boolean
string
number
bigint // 大整数 ,与number不兼容
symbol
object
undefined
null

let x: any;
let var_name: string = "String";
function fuc(a:string ,b?:string) {

}
function fn(x: string | number)
let gender: "male" | "female";


// any 的污染问题
let x: any = "hello";
let y: number;

y = x; // 不报错

y * 123; // 不报错
y.toFixed(); // 不报错
// solvor: unknown
let v: unknown = 123;

let v1: boolean = v; // 报错
let v2: number = v; // 报错
// never
never类型的一个重要特点是,可以赋值给任意其他类型。
function f(): never {
  throw new Error("Error");
}

let v1: number = f(); // 不报错
let v2: string = f(); // 不报错
let v3: boolean = f(); // 不报错


// 含包装对象与不含包装对象
"hello"; // 字面量
new String("hello"); // 包装对象

Boolean  boolean
String  string
Number  number
BigInt  bigint
Symbol  symbol


// 交叉类型
let obj: { foo: string } & { bar: string };

obj = {
  foo: "hello",
  bar: "world",
};


// type
type Age = number;

let age: Age = 55;


//数组
let arr: (number | string)[];


//元组
type t = readonly [number, string];
type myTuple = [number, number, number?, string?];

type t1 = [string, number, ...boolean[]];
type t2 = [string, ...boolean[], number];
type t3 = [...boolean[], string, number];


// 函数
// 写法一
const hello = function (txt: string) {
  console.log("hello " + txt);
};

// 写法二
const hello: (txt: string) => void = function (txt) {
  console.log("hello " + txt);

const repeat = (str: string, times: number): string => str.repeat(times);
};
function f(x?: number)// 可选参数
// 函数结构
function sum({ a, b, c }: { a: number; b: number; c: number }) {
  console.log(a + b + c);
}


// 类型断言
type T = "a" | "b" | "c";

let foo = "a";
let bar: T = foo as T; // 正确

// interface
interface Person {
  firstName: string;
  lastName: string;
  age: number;
}

// 类
class Point {
  x: number;
  y: number;

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }

  add(point: Point) {
    return new Point(this.x + point.x, this.y + point.y);
  }
}

// 泛型
function getFirst<T>(arr: T[]): T {
  return arr[0];
}

hooks

useState

自动触发渲染

useRef

不自动触发渲染

1
2
const ref = useRef(0);
ref.current 属性访问该 ref 的当前值

useContext (自己可以实现一个Zustand)

上下文钩子 Usage:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 首先要将 provider 包裹App,你也可以包在需要的组件上,只是这样方便能把一些库的Provider放一起管理()
const AppContext = React.createContext({});
<AppContext.Provider value={{
  username: '666'
}}>
    <App/>
</AppContext.Provider>

// 在其他组件的使用
const { username } = useContext(AppContext)

可以放在那里

1.自定义Toast组件 2.主题切换

useEffect

副作用钩子 + 首次加载时执行

1
2
3
    useEffect(()=>{

    },[需要添加副作用的变量]);

灵感

1.用于从后端加载数据

useCallback

缓存函数(组件),防止造成不必要的重新渲染.
保持函数的引用稳定

1
2
3
4
 // 缓存回调函数
  const handleItemClick = useCallback((item) => {
    alert("你点击了:" + item);
  }, []);

useMemo

缓存变量

1
2
3
4
5
6
  const filteredItems = useMemo(() => {
    console.log("过滤列表");
    return ["苹果", "香蕉", "橙子", "西瓜"].filter(item =>
      item.includes(filter)
    );
  }, [filter]);

随便写个组件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React, { useState } from 'react';

const CardComponent = ({ children }) => {
  const [isHighlighted, setIsHighlighted] = useState(false);

  const toggleHighlight = () => {
    setIsHighlighted(!isHighlighted);
  };

  return (
    <div
      className={`p-6 border rounded-lg shadow-md transition-all duration-300 ${
        isHighlighted ? 'bg-blue-100 border-blue-500' : 'bg-white border-gray-300'
      }`}
    >
      <h2 className="text-xl font-semibold mb-4">Card Title</h2>
      <div className="mb-4">
        {children}
      </div>
      <button
        onClick={toggleHighlight}
        className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 focus:outline-none"
      >
        {isHighlighted ? 'Remove Highlight' : 'Highlight'}
      </button>
    </div>
  );
};

export default CardComponent;

zustand

一般业务场景只有管理auth时才会用,所以找个模板直接抄就行(bushi) 其实redux也简单,但是一上到写addCase的程度就麻烦起来了(兄啊,不写这个的话好像也没必要用这个吧)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// stores/authStore.js
import { create } from 'zustand';

const useAuthStore = create((set) => {
  const savedToken = localStorage.getItem('token');
  const savedUser = savedToken ? { id: 1, name: 'Alice' } : null;

  return {
    user: savedUser,
    token: savedToken,
    isAuthenticated: !!savedToken,

    login: (userData, token) => {
      localStorage.setItem('token', token);
      set({ user: userData, token, isAuthenticated: true });
    },

    logout: () => {
      localStorage.removeItem('token');
      set({ user: null, token: null, isAuthenticated: false });
    },
  };
});

export default useAuthStore;

// 使用
// components/UserInfo.jsx
import React from 'react';
import useAuthStore from '../stores/authStore';

const UserInfo = () => {
  const { user, isAuthenticated } = useAuthStore();

  return (
    <div>
      {isAuthenticated ? (
        <p>Welcome, {user.name}</p>
      ) : (
        <p>You are not logged in.</p>
      )}
    </div>
  );
};

export default UserInfo;
// 处理
// components/LogoutButton.jsx
import React from 'react';
import useAuthStore from '../stores/authStore';

const LogoutButton = () => {
  const logout = useAuthStore((state) => state.logout);

  return <button onClick={logout}>Logout</button>;
};

export default LogoutButton;

tailwind

pre

1
2
npm install -D tailwindcss
npx tailwindcss init
1
2
3
4
5
6
7
8
9
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./src/**/*.{html,js}"],
  theme: {
    extend: {},
  },
  plugins: [],
}
1
2
3
4
/*src/input.css*/
@tailwind base;
@tailwind components;
@tailwind utilities;
1
npx tailwindcss -i ./src/input.css -o ./src/output.css --watch

基础

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<div class="px-2 mx-2 border border-gray-300 rounded-full dark:... hover:... focus:... ring(环) ring-[#fff] acitve:">
  <Text class="font-bold text-xl text-gray-300">...</Text>
</div>

<div class="flex-1 flex-row first:pt-0 last:pb-0 odd:bg-white(奇数) even:bg-slate-50(偶数)(数组控制) selection:bg-fuchsia-300 selection:text-fuchsia-900"></div>

<input type="" name="" class="disabled: focus: invaild: placeholder:"/>

<div class="group"> // 父元素变化传递
  <div class="group-hover:"></div>
</div>

<input type="file" class="block
  file:mr-4 file:py-2 file:px-4
  file:rounded-full file:border-0
  file:text-sm file:font-semibold
  file:bg-violet-50 file:text-violet-700
  hover:file:bg-violet-100
"/>

<p class="first-line:uppercase first-line:tracking-widest
  first-letter:text-7xl first-letter:font-bold first-letter:text-slate-900
  first-letter:mr-3 first-letter:float-left
">
  Well, let me tell you something, funny boy. Y'know that little stamp, the one
  that says "New York Public Library"? Well that may not mean anything to you,
  but that means a lot to me. One whole hell of a lot.
</p>

<div class="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-6">
  <!-- ... -->
</div>

<div class="grid grid-cols-4 gap-4">
  <div>01</div>
  <!-- ... -->
  <div>05</div>
  <div class="grid grid-cols-subgrid gap-4 col-span-3">
    <div class="col-start-2">06</div>
  </div>
</div>

// 效果
<div class="blur-lg drop-shadow-sm"></div>
// 渐变
<div class="bg-gradient-to-r from-cyan-500 to-blue-500 w-full h-64">
  <!-- 内容 -->
</div>

响应式

注意:tailwind 中断点是min而不是max

1
2
3
4
5
6
7
| Breakpoint prefix | Minimum width | CSS                                  |
|-------------------|---------------|--------------------------------------|
| `sm`              | 640px         | `@media (min-width: 640px) { ... }`  |
| `md`              | 768px         | `@media (min-width: 768px) { ... }`  |
| `lg`              | 1024px        | `@media (min-width: 1024px) { ... }` |
| `xl`              | 1280px        | `@media (min-width: 1280px) { ... }` |
| `2xl`             | 1536px        | `@media (min-width: 1536px) { ... }` |
1
<div class="text-center sm:text-left"></div>

定制

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/** @type {import('tailwindcss').Config} */
module.exports = {
  theme: {
    screens: {
      sm: '480px',
      md: '768px',
      lg: '976px',
      xl: '1440px',
    },
    colors: {
      'blue': '#1fb6ff',
      'purple': '#7e5bef',
      'pink': '#ff49db',
      'orange': '#ff7849',
      'green': '#13ce66',
      'yellow': '#ffc82c',
      'gray-dark': '#273444',
      'gray': '#8492a6',
      'gray-light': '#d3dce6',
    },
    fontFamily: {
      sans: ['Graphik', 'sans-serif'],
      serif: ['Merriweather', 'serif'],
    },
    extend: {
      spacing: {
        '128': '32rem',
        '144': '36rem',
      },
      borderRadius: {
        '4xl': '2rem',
      }
    }
  }
}

socket.io

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  useEffect(() => {
    if (!user || !accessToken || !chatId) return;

    const newSocket = io(BASE_INFO.BASE_SOCKET_URL, {
      auth: {
        token: accessToken,
        room_id: String(chatId),
      },
      transports: ['websocket'],
      reconnectionAttempts: Infinity,
    });
    // 认证失败拦截器
    setupSocketIOInterceptor(newSocket);
    
    newSocket.on('connect', () => {
      console.log('Socket 已连接');
    });

    newSocket.on('disconnect', (reason) => {
        console.log('Socket 断开连接:', reason);
        if (reason === 'io server disconnect') {
          newSocket.connect(); // 主动重连
        }
      });

    newSocket.on('receive:message', handleNewMessage);
    newSocket.on('message:error', handleMessageError);

    setSocket(newSocket);

    return () => {
      newSocket.off('receive:message', handleNewMessage);
      newSocket.off('message:error', handleMessageError);
      newSocket.disconnect();
    };
  }, [user, accessToken, chatId, handleNewMessage, handleMessageError]);

拦截器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import { getItemFromAsyncStorage, setItemToAsyncStorage } from "./LocalStorage";
import { refreshAccessToken } from "./LoginUtil";

let isRefreshing = false;
let failedQueue = [];

const processQueue = (error, token = null) => {
  failedQueue.forEach((prom) => {
    if (error) {
      prom.reject(error);
    } else {
      prom.resolve(token);
    }
  });

  failedQueue = [];
};

export function setupSocketIOInterceptor(socket) {
  const originalConnect = socket.connect.bind(socket);
  socket.connect = function (...args) {
    socket.on('connect_error', async (error) => {
      if (error.message.includes('jwt expired') || error.message.includes('invalid token')) {
        if (isRefreshing) {
          return new Promise((resolve, reject) => {
            failedQueue.push({ resolve, reject });
          })
            .then((token) => {
              socket.auth.token = token;
              originalConnect();
            })
            .catch((err) => {
              console.error('Failed to refresh token:', err);
            });
        }

        isRefreshing = true;

        try {
          const oldRefreshToken = await getItemFromAsyncStorage("refreshToken");
          const { refreshToken, accessToken } = await refreshAccessToken(oldRefreshToken);

          await setItemToAsyncStorage("accessToken", accessToken);
          await setItemToAsyncStorage("refreshToken", refreshToken);

          socket.auth.token = accessToken;
          processQueue(null, accessToken);
          
          // 重新连接
          originalConnect();
        } catch (err) {
          processQueue(err, null);
          console.error("刷新 token 失败:", err);
        } finally {
          isRefreshing = false;
        }
      }
    });

    originalConnect(...args);
  };

  return socket;
}

axios

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const response = axios.get(`BASE_URL`);
axios.post(
  'https://xxx.com',
  { param1, param2 },
  {
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    }
  }
);

axios.post('https://api.example.com/submit', {
  firstName: 'Alice',
  lastName: 'Smith'
})
.then(response => console.log(response.data))
.catch(error => console.error(error));

Electron

main

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import { app, BrowserWindow, ipcMain, fs, path } from 'electron';

let mainWindow;

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1000,
    height: 600,
    webPreferences: {
      preload: path.resolve('preload.js'),
      contextIsolation: true,
      sandbox: true
    }
  });

  mainWindow.loadFile('index.html');
  // mainWindow.webContents.openDevTools();
}

app.whenReady().then(createWindow);

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow();
  }
});

// 🔄 双向通信:渲染进程请求,主进程响应
ipcMain.handle('read-file', async (event, filePath) => {
  try {
    const fullPath = path.resolve(filePath);
    const data = await fs.promises.readFile(fullPath, 'utf-8');
    return { success: true, content: data };
  } catch (err) {
    return { success: false, error: err.message };
  }
});

// 📢 主进程主动发送消息给渲染进程
setInterval(() => {
  if (mainWindow && !mainWindow.isDestroyed()) {
    mainWindow.webContents.send('main-to-renderer', {
      message: '这是主进程主动发送的消息!',
      timestamp: Date.now()
    });
  }
}, 5000); // 每5秒推送一次

preload

1
2
3
4
5
6
7
8
import { contextBridge, ipcRenderer } from 'electron';

contextBridge.exposeInMainWorld('electronAPI', {
  // 🔄 双向通信:调用主进程并等待结果
  readFile: (filePath) => ipcRenderer.invoke('read-file', filePath),
  // 💬 主进程推送:注册监听器
  onMessageFromMain: (callback) => ipcRenderer.on('main-to-renderer', callback)
});

renderer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
document.addEventListener('DOMContentLoaded', () => {
  const output = document.getElementById('output');
  const input = document.getElementById('file-path');

  // 💬 主进程主动发送的消息监听
  window.electronAPI.onMessageFromMain((event, message) => {
    const msgDiv = document.createElement('div');
    msgDiv.textContent = `[主进程消息] ${message.message} - ${new Date(message.timestamp).toLocaleTimeString()}`;
    document.body.appendChild(msgDiv);
  });

  // 🔄 渲染进程请求主进程读取文件
  document.getElementById('btn-read').addEventListener('click', async () => {
    const filePath = input.value;
    if (!filePath) return;

    try {
      const result = await window.electronAPI.readFile(filePath);
      if (result.success) {
        output.textContent = result.content;
      } else {
        output.textContent = '读取失败:' + result.error;
      }
    } catch (err) {
      output.textContent = '发生错误:' + err;
    }
  });
});
Licensed under CC BY-NC-SA 4.0