mirror of
https://github.com/pmndrs/zustand.git
synced 2025-12-08 19:45:52 +00:00
Update docs related to SSR/Hydration and SSR Apps (#2298)
* Update deprecation comments * Update testing guide * WIP * Minor fixes * WIP * Update docs/guides/ssr-and-hygration.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update docs/guides/ssr-and-hygration.md Co-authored-by: Blazej Sewera <code@sewera.dev> * WIP * Minor changes * Minor changes * Update nextjs.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update nextjs.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update nextjs.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update nextjs.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update nextjs.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update nextjs.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update nextjs.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update nextjs.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update nextjs.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update nextjs.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update nextjs.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update nextjs.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update nextjs.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update nextjs.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update nextjs.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update nextjs.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update nextjs.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update nextjs.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update nextjs.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update ssr-and-hygration.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update ssr-and-hygration.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update ssr-and-hygration.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update ssr-and-hygration.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update ssr-and-hygration.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update ssr-and-hygration.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update ssr-and-hygration.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update ssr-and-hygration.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update ssr-and-hygration.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update docs/guides/nextjs.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update docs/guides/ssr-and-hygration.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update docs/guides/nextjs.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Minor fixes * Update docs/guides/nextjs.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update docs/guides/nextjs.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update docs/guides/ssr-and-hydration.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update docs/guides/ssr-and-hydration.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update docs/guides/ssr-and-hydration.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update docs/guides/ssr-and-hydration.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update docs/guides/nextjs.md Co-authored-by: Blazej Sewera <code@sewera.dev> * Update docs/guides/nextjs.md Co-authored-by: Charles Kornoelje <33156025+charkour@users.noreply.github.com> * Update docs/guides/ssr-and-hydration.md Co-authored-by: Charles Kornoelje <33156025+charkour@users.noreply.github.com> * Update ssr-and-hydration.md * Update docs/guides/ssr-and-hydration.md Co-authored-by: Charles Kornoelje <33156025+charkour@users.noreply.github.com> --------- Co-authored-by: Blazej Sewera <code@sewera.dev> Co-authored-by: Charles Kornoelje <33156025+charkour@users.noreply.github.com>
This commit is contained in:
parent
27bffb1fba
commit
f64a111b85
277
docs/guides/nextjs.md
Normal file
277
docs/guides/nextjs.md
Normal file
@ -0,0 +1,277 @@
|
||||
---
|
||||
title: Setup with Next.js
|
||||
nav: 21
|
||||
---
|
||||
|
||||
[Next.js](https://nextjs.org) is a popular server-side rendering framework for React that presents
|
||||
some unique challenges for using Zustand properly.
|
||||
Keep in mind that Zustand store is a global
|
||||
variable (AKA module state) making it optional to use a `Context`.
|
||||
These challenges include:
|
||||
|
||||
- **Per-request store:** A Next.js server can handle multiple requests simultaneously. This means
|
||||
that the store should be created per request and should not be shared across requests.
|
||||
- **SSR friendly:** Next.js applications are rendered twice, first on the server
|
||||
and again on the client. Having different outputs on both the client and the server will result
|
||||
in "hydration errors." The store will have to be initialized on the server and then
|
||||
re-initialized on the client with the same data in order to avoid that. Please read more about
|
||||
that in our [SSR and Hydration](./ssr-and-hygration) guide.
|
||||
- **SPA routing friendly:** Next.js supports a hybrid model for client side routing, which means
|
||||
that in order to reset a store, we need to intialize it at the component level using a
|
||||
`Context`.
|
||||
- **Server caching friendly:** Recent versions of Next.js (specifically applications using the App
|
||||
Router architecture) support aggressive server caching. Due to our store being a **module state**,
|
||||
it is completely compatible with this caching.
|
||||
|
||||
We have these general recommendations for the appropriate use of Zustand:
|
||||
|
||||
- **No global stores** - Because the store should not be shared across requests, it should not be defined
|
||||
as a global variable. Instead, the store should be created per request.
|
||||
- **React Server Components should not read from or write to the store** - RSCs cannot use hooks or context. They aren't
|
||||
meant to be stateful. Having an RSC read from or write values to a global store violates the
|
||||
architecture of Next.js.
|
||||
|
||||
### Creating a store per request
|
||||
|
||||
Let's write our store factory function that will create a new store for each
|
||||
request.
|
||||
|
||||
```ts
|
||||
// stores/counter-store.ts
|
||||
import { createStore } from 'zustand'
|
||||
|
||||
export type CounterState = {
|
||||
count: number
|
||||
}
|
||||
|
||||
export type CounterActions = {
|
||||
decrementCount: () => void
|
||||
incrementCount: () => void
|
||||
}
|
||||
|
||||
export type CounterStore = CounterState & CounterActions
|
||||
|
||||
export const defaultInitState: Partial<CounterState> = {
|
||||
count: 0,
|
||||
}
|
||||
|
||||
export const createCounterStore = (
|
||||
initState: Partial<CounterState> = defaultInitState,
|
||||
) => {
|
||||
return createStore<CounterStore>()((set) => ({
|
||||
...initState,
|
||||
decrementCount: () => set((state) => ({ count: state.count - 1 })),
|
||||
incrementCount: () => set((state) => ({ count: state.count + 1 })),
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
### Providing the store
|
||||
|
||||
Let's use the `createCounterStore` in our component and share it using a context provider.
|
||||
|
||||
```tsx
|
||||
// components/counter-store-provider.tsx
|
||||
'use client'
|
||||
|
||||
import { type ReactNode, createContext, useRef, useContext } from 'react'
|
||||
|
||||
import { type CounterStore, createCounterStore } from 'stores/counter-store.ts'
|
||||
|
||||
export const CounterStoreContext = createContext()
|
||||
|
||||
export interface CounterStoreProviderProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const CounterStoreProvider = ({
|
||||
children,
|
||||
}: CounterStoreProviderProps) => {
|
||||
const storeRef = useRef<CounterStore>()
|
||||
if (!storeRef.current) {
|
||||
storeRef.current = createCounterStore()
|
||||
}
|
||||
|
||||
return (
|
||||
<CounterStoreContext.Provider value={storeRef.current}>
|
||||
{children}
|
||||
</CounterStoreContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useCounterStore = (selector: (store: CounterStore) => T): T => {
|
||||
const counterStoreContext = useContext(CounterStoreContext)
|
||||
|
||||
if (counterStoreContext === undefined) {
|
||||
throw new Error(`useCounterStore must be use within CounterStoreProvider`)
|
||||
}
|
||||
|
||||
return useStore(counterStoreContext, selector)
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** In this example, we ensure that this component is re-render-safe by checking the
|
||||
> value of the reference, so that the store is only created once. This component will only be
|
||||
> rendered once per request on the server, but might be re-rendered multiple times on the client if
|
||||
> there are stateful client components located above this component in the tree, or if this component
|
||||
> also contains other mutable state that causes a re-render.
|
||||
|
||||
### Initializing the store
|
||||
|
||||
```ts
|
||||
// stores/counter-store.ts
|
||||
// rest of code
|
||||
|
||||
export const initCounterStore = (): CounterState => {
|
||||
return { count: new Date().getFullYear() }
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// components/counter-store-provider.tsx
|
||||
// rest of code
|
||||
|
||||
import {
|
||||
type CounterStore,
|
||||
createCounterStore,
|
||||
initCounterStore,
|
||||
} from 'stores/counter-store.ts'
|
||||
|
||||
// rest of code
|
||||
|
||||
export const CounterStoreProvider = ({
|
||||
children,
|
||||
}: CounterStoreProviderProps) => {
|
||||
const storeRef = useRef<CounterStore>()
|
||||
if (!storeRef.current) {
|
||||
storeRef.current = createCounterStore(initCounterStore())
|
||||
}
|
||||
|
||||
return (
|
||||
<CounterStoreContext.Provider value={storeRef.current}>
|
||||
{children}
|
||||
</CounterStoreContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// rest of code
|
||||
```
|
||||
|
||||
### Using the store with different architectures
|
||||
|
||||
There are two architectures for a Next.js application: the
|
||||
[Pages Router](https://nextjs.org/docs/pages/building-your-application/routing) and the
|
||||
[App Router](https://nextjs.org/docs/app/building-your-application/routing). The usage of Zustand on
|
||||
both architectures should be the same with slight differences related to each architecture.
|
||||
|
||||
#### Pages Router
|
||||
|
||||
```tsx
|
||||
// components/pages/home-page.tsx
|
||||
import { useCounterStore } from 'components/counter-store-provider.ts'
|
||||
|
||||
export const HomePage = () => {
|
||||
const { count, incrementCount, decrementCount } = useCounterStore()
|
||||
|
||||
return (
|
||||
<div>
|
||||
Count: {count}
|
||||
<hr />
|
||||
<button type="button" onClick={() => void incrementCount()}>
|
||||
Increment Count
|
||||
</button>
|
||||
<button type="button" onClick={() => void decrementCount()}>
|
||||
Decrement Count
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// _app.tsx
|
||||
import type { AppProps } from 'next/app'
|
||||
|
||||
import { CounterStoreProvider } from 'components/counter-store-provider.tsx'
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<CounterStoreProvider>
|
||||
<Component {...pageProps} />
|
||||
</CounterStoreProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** creating a store per route would require creating and sharing the store
|
||||
> at page (route) component level. Try not to use this if you do not need to create
|
||||
> a store per route.
|
||||
|
||||
```tsx
|
||||
// pages/index.tsx
|
||||
import { CounterStoreProvider } from 'components/counter-store-provider.tsx'
|
||||
import { HomePage } from 'components/pages/home-page.tsx'
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<CounterStoreProvider>
|
||||
<HomePage />
|
||||
</CounterStoreProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### App Router
|
||||
|
||||
```tsx
|
||||
// components/pages/home-page.tsx
|
||||
import { useCounterStore } from 'components/counter-store-provider.ts'
|
||||
|
||||
export const HomePage = () => {
|
||||
const { count, incrementCount, decrementCount } = useCounterStore()
|
||||
|
||||
return (
|
||||
<div>
|
||||
Count: {count}
|
||||
<hr />
|
||||
<button type="button" onClick={() => void incrementCount()}>
|
||||
Increment Count
|
||||
</button>
|
||||
<button type="button" onClick={() => void decrementCount()}>
|
||||
Decrement Count
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// app/layout.tsx
|
||||
import { type ReactNode } from 'react'
|
||||
|
||||
export interface RootLayoutProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }: RootLayoutProps) {
|
||||
return <CounterStoreProvider>{children}</CounterStoreProvider>
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** creating a store per route would require creating and sharing the store
|
||||
> at page (route) component level. Try not to use this if you do not need to create
|
||||
> a store per route.
|
||||
|
||||
```tsx
|
||||
// app/index.tsx
|
||||
import { CounterStoreProvider } from 'components/counter-store-provider.tsx'
|
||||
import { HomePage } from 'components/pages/home-page.tsx'
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<CounterStoreProvider>
|
||||
<HomePage />
|
||||
</CounterStoreProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
198
docs/guides/ssr-and-hydration.md
Normal file
198
docs/guides/ssr-and-hydration.md
Normal file
@ -0,0 +1,198 @@
|
||||
---
|
||||
title: SSR and Hydration
|
||||
nav: 20
|
||||
---
|
||||
|
||||
## Server-side Rendering (SSR)
|
||||
|
||||
Server-side Rendering (SSR) is a technique that helps us render our components into
|
||||
HTML strings on the server, send them directly to the browser, and finally "hydrate" the
|
||||
static markup into a fully interactive app on the client.
|
||||
|
||||
### React
|
||||
|
||||
Let's say we want to render a stateless app using React. In order to do that, we need
|
||||
to use `express`, `react` and `react-dom/server`. We don't need `react-dom/client`
|
||||
since it's a stateless app.
|
||||
|
||||
Let's dive into that:
|
||||
|
||||
- `express` helps us build a web app that we can run using Node,
|
||||
- `react` helps us build the UI components that we use in our app,
|
||||
- `react-dom/server` helps us render our components on a server.
|
||||
|
||||
```json
|
||||
// tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"noImplicitAny": false,
|
||||
"noEmitOnError": true,
|
||||
"removeComments": false,
|
||||
"sourceMap": true,
|
||||
"target": "esnext"
|
||||
},
|
||||
"include": ["**/*"]
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** do not forget to remove all comments from your `tsconfig.json` file.
|
||||
|
||||
```tsx
|
||||
// app.tsx
|
||||
export const App = () => {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Static Server-side-rendered App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div>Hello World!</div>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// server.tsx
|
||||
import express from 'express'
|
||||
import React from 'react'
|
||||
import ReactDOMServer from 'react-dom/server'
|
||||
|
||||
import { App } from './app.tsx'
|
||||
|
||||
const port = Number.parseInt(process.env.PORT || '3000', 10)
|
||||
const app = express()
|
||||
|
||||
app.get('/', (_, res) => {
|
||||
const { pipe } = ReactDOMServer.renderToPipeableStream(<App />, {
|
||||
onShellReady() {
|
||||
res.setHeader('content-type', 'text/html')
|
||||
pipe(res)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server is listening at ${port}`)
|
||||
})
|
||||
```
|
||||
|
||||
```sh
|
||||
tsc --build
|
||||
```
|
||||
|
||||
```sh
|
||||
node server.js
|
||||
```
|
||||
|
||||
## Hydration
|
||||
|
||||
Hydration turns the initial HTML snapshot from the server into a fully interactive app
|
||||
that runs in the browser. The right way to "hydrate" a component is by using `hydrateRoot`.
|
||||
|
||||
### React
|
||||
|
||||
Let's say we want to render an statefull app using react. In order to do that we need to
|
||||
use `express`, `react`, `react-dom/server` and `react-dom/client`.
|
||||
|
||||
Let's dive into that:
|
||||
|
||||
- `express` helps us build a web app that we can run using Node,
|
||||
- `react` helps us build the UI components that we use in our app,
|
||||
- `react-dom/server` helps us render our components on a server,
|
||||
- `react-dom/client` helps us hydrate our components on a client.
|
||||
|
||||
> **Note:** Do not forget that even if we can render our components on a server, it is
|
||||
> important to "hydrate" them on a client to make them interactive.
|
||||
|
||||
```json
|
||||
// tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"noImplicitAny": false,
|
||||
"noEmitOnError": true,
|
||||
"removeComments": false,
|
||||
"sourceMap": true,
|
||||
"target": "esnext"
|
||||
},
|
||||
"include": ["**/*"]
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** do not forget to remove all comments in your `tsconfig.json` file.
|
||||
|
||||
```tsx
|
||||
// app.tsx
|
||||
export const App = () => {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Static Server-side-rendered App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div>Hello World!</div>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// main.tsx
|
||||
import ReactDOMClient from 'react-dom/client'
|
||||
|
||||
import { App } from './app.tsx'
|
||||
|
||||
ReactDOMClient.hydrateRoot(<App />, document)
|
||||
```
|
||||
|
||||
```tsx
|
||||
// server.tsx
|
||||
import express from 'express'
|
||||
import React from 'react'
|
||||
import ReactDOMServer from 'react-dom/server'
|
||||
|
||||
import { App } from './app.tsx'
|
||||
|
||||
const port = Number.parseInt(process.env.PORT || '3000', 10)
|
||||
const app = express()
|
||||
|
||||
app.use('/', (_, res) => {
|
||||
const { pipe } = ReactDOMServer.renderToPipeableStream(<App />, {
|
||||
bootstrapScripts: ['/main.js'],
|
||||
onShellReady() {
|
||||
res.setHeader('content-type', 'text/html')
|
||||
pipe(res)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server is listening at ${port}`)
|
||||
})
|
||||
```
|
||||
|
||||
```sh
|
||||
tsc --build
|
||||
```
|
||||
|
||||
```sh
|
||||
node server.js
|
||||
```
|
||||
|
||||
> **Warning:** The React tree you pass to `hydrateRoot` needs to produce the same output as it did on the server.
|
||||
> The most common causes leading to hydration errors include:
|
||||
>
|
||||
> - Extra whitespace (like newlines) around the React-generated HTML inside the root node.
|
||||
> - Using checks like typeof window !== 'undefined' in your rendering logic.
|
||||
> - Using browser-only APIs like `window.matchMedia` in your rendering logic.
|
||||
> - Rendering different data on the server and the client.
|
||||
>
|
||||
> React recovers from some hydration errors, but you must fix them like other bugs. In the best case, they’ll lead to a slowdown; in the worst case, event handlers can get attached to the wrong elements.
|
||||
|
||||
You can read more about the caveats and pitfalls here: [hydrateRoot](https://react.dev/reference/react-dom/client/hydrateRoot)
|
||||
@ -79,7 +79,7 @@ export const storeResetFns = new Set<() => void>()
|
||||
|
||||
const createUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
|
||||
const store = actualCreate(stateCreator)
|
||||
const initialState = store.getState()
|
||||
const initialState = store.getInitialState()
|
||||
storeResetFns.add(() => {
|
||||
store.setState(initialState, true)
|
||||
})
|
||||
@ -98,7 +98,7 @@ export const create = (<T>(stateCreator: zustand.StateCreator<T>) => {
|
||||
|
||||
const createStoreUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
|
||||
const store = actualCreateStore(stateCreator)
|
||||
const initialState = store.getState()
|
||||
const initialState = store.getInitialState()
|
||||
storeResetFns.add(() => {
|
||||
store.setState(initialState, true)
|
||||
})
|
||||
@ -168,7 +168,7 @@ export const storeResetFns = new Set<() => void>()
|
||||
|
||||
const createUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
|
||||
const store = actualCreate(stateCreator)
|
||||
const initialState = store.getState()
|
||||
const initialState = store.getInitialState()
|
||||
storeResetFns.add(() => {
|
||||
store.setState(initialState, true)
|
||||
})
|
||||
@ -187,7 +187,7 @@ export const create = (<T>(stateCreator: zustand.StateCreator<T>) => {
|
||||
|
||||
const createStoreUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
|
||||
const store = actualCreateStore(stateCreator)
|
||||
const initialState = store.getState()
|
||||
const initialState = store.getInitialState()
|
||||
storeResetFns.add(() => {
|
||||
store.setState(initialState, true)
|
||||
})
|
||||
|
||||
@ -23,7 +23,7 @@ type ExtractState<S> = S extends { getState: () => infer T } ? T : never
|
||||
type ReadonlyStoreApi<T> = Pick<StoreApi<T>, 'getState' | 'subscribe'>
|
||||
|
||||
type WithReact<S extends ReadonlyStoreApi<unknown>> = S & {
|
||||
/** @deprecated please use api.getState() */
|
||||
/** @deprecated please use api.getInitialState() */
|
||||
getServerState?: () => ExtractState<S>
|
||||
}
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ type ExtractState<S> = S extends { getState: () => infer T } ? T : never
|
||||
type ReadonlyStoreApi<T> = Pick<StoreApi<T>, 'getState' | 'subscribe'>
|
||||
|
||||
type WithReact<S extends ReadonlyStoreApi<unknown>> = S & {
|
||||
/** @deprecated please use api.getState() */
|
||||
/** @deprecated please use api.getInitialState() */
|
||||
getServerState?: () => ExtractState<S>
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user