docs: Add React reducer code snippets and config updates

This commit is contained in:
Gauthier Daniels 2025-04-13 16:53:40 +02:00
parent 68344817ae
commit 02f81adaf3
16 changed files with 140 additions and 161 deletions

View File

@ -1,46 +1,19 @@
import { Highlight, Prism } from "prism-react-renderer";
import { prismThemes } from "@/data/themes/prism";
import { useTheme } from "@/hooks/useTheme";
import { Fragment, useMemo } from "react";
import { clientOnly } from "vike-react/clientOnly";
import { SSRSnippet } from "./SSRSnippet";
const CSRSnippet = clientOnly(() => import("./CSRSnippet"));
function SSRFence({ children, language }: { children: string; language: string }) {
const { theme } = useTheme();
const prismTheme = useMemo(() => {
return prismThemes[theme];
}, [theme]);
return (
<Highlight code={children.trimEnd()} language={language} theme={prismTheme} prism={Prism}>
{({ className, style, tokens, getTokenProps }) => (
<pre className={className} style={style}>
<code>
{tokens.map((line, lineIndex) => (
<Fragment key={lineIndex}>
{line
.filter((token) => !token.empty)
.map((token, tokenIndex) => (
<span key={tokenIndex} {...getTokenProps({ token })} />
))}
{"\n"}
</Fragment>
))}
</code>
</pre>
)}
</Highlight>
);
}
export function Fence({ children, language }: { children: string; language: string }) {
const props = {
language,
label: undefined,
showLineNumbers: false,
children,
};
return (
<div className="relative group">
<CSRSnippet language={language} fallback={<SSRFence language={language} children={children} />}>
{children}
</CSRSnippet>
<CSRSnippet {...props} fallback={<SSRSnippet {...props} />} />
</div>
);
}

View File

@ -0,0 +1,58 @@
import { Highlight, Prism } from "prism-react-renderer";
import { prismThemes } from "@/data/themes/prism";
import { useTheme } from "@/hooks/useTheme";
import { Fragment, useMemo } from "react";
import clsx from "clsx";
export function SSRSnippet({
children,
language,
label,
showLineNumbers = false,
}: {
children: string;
language: string;
label?: string;
showLineNumbers?: boolean;
}) {
const { theme } = useTheme();
const prismTheme = useMemo(() => {
return prismThemes[theme];
}, [theme]);
return (
<Highlight code={children.trimEnd()} language={language} theme={prismTheme} prism={Prism}>
{({ className, style, tokens, getTokenProps }) => (
<div className="relative w-full">
{label && (
<div className="absolute px-4 py-1 left-0 text-sm text-gray-700 dark:text-gray-200 italic w-full bg-gray-200 dark:bg-gray-700 rounded-t-xl">
{label}
</div>
)}
<pre className={clsx(className, { "pt-11": !!label })} style={style}>
<code>
{tokens.map((line, lineIndex) => (
<Fragment key={lineIndex}>
{showLineNumbers && (
<span
className="text-gray-400 dark:text-gray-500 text-right font-mono w-8 inline-block pr-4"
style={{ userSelect: "none" }}
>
{lineIndex + 1}
</span>
)}
{line
.filter((token) => !token.empty)
.map((token, tokenIndex) => (
<span key={tokenIndex} {...getTokenProps({ token })} />
))}
{"\n"}
</Fragment>
))}
</code>
</pre>
</div>
)}
</Highlight>
);
}

View File

@ -1,57 +1,11 @@
import type { Data } from "@/pages/docs/+data";
import { snippetsService } from "@/services/SnippetsService";
import { Highlight, Prism } from "prism-react-renderer";
import { clientOnly } from "vike-react/clientOnly";
import { prismThemes } from "@/data/themes/prism";
import { useData } from "vike-react/useData";
import { useTheme } from "@/hooks/useTheme";
import { Fragment, useMemo } from "react";
import { SSRSnippet } from "./SSRSnippet";
const CSRSnippet = clientOnly(() => import("./CSRSnippet"));
function SSRSnippet({
language,
children,
label,
showLineNumbers = false,
}: {
language: string;
children: string;
label?: string;
showLineNumbers?: boolean;
}) {
const { theme } = useTheme();
const prismTheme = useMemo(() => {
return prismThemes[theme];
}, [theme]);
return (
<>
{label && <div className="text-sm text-gray-500 dark:text-gray-400 mb-2">{label}</div>}
<Highlight code={children.trimEnd()} language={language} theme={prismTheme} prism={Prism}>
{({ className, style, tokens, getTokenProps }) => (
<pre className={className} style={style}>
<code>
{tokens.map((line, lineIndex) => (
<Fragment key={lineIndex}>
{line
.filter((token) => !token.empty)
.map((token, tokenIndex) => (
<span key={tokenIndex} {...getTokenProps({ token })} />
))}
{"\n"}
</Fragment>
))}
</code>
</pre>
)}
</Highlight>
</>
);
}
export function Snippet({
path,
language,

View File

@ -47,19 +47,13 @@ Comme expliqué plus tôt, un reducer est une fonction qui prend en paramètre u
Parlons dans un premier temps de la signature d'un reducer :
{% tabs defaultSelectedTab="jsx" %}
{% tab value="jsx" label="JSX" %}
{% snippet path="data/docs/react/usereducer/reducer-example.jsx" language="jsx" showLineNumbers=true /%}
{% snippet path="react/reducer/reducer-example.jsx" language="jsx" showLineNumbers=true /%}
{% /tab %}
{% tab value="tsx" label="TSX" %}
{% snippet path="data/docs/react/usereducer/reducer-example.tsx" language="tsx" showLineNumbers=true /%}
{% snippet path="react/reducer/reducer-example.tsx" language="tsx" showLineNumbers=true /%}
{% /tab %}
{% /tabs %}
Comme tu peux le voir, on récupère bien deux paramètres : `state` et `action`.
@ -82,7 +76,7 @@ En déversant le contenu de l'état actuel, on s'assure de ne pas perdre ces pro
Par exemple :
{% snippet path="data/docs/react/usereducer/reducer-why-spread-operator.jsx" language="jsx" showLineNumbers=true /%}
{% snippet path="react/reducer/reducer-why-spread-operator.jsx" language="jsx" showLineNumbers=true /%}
On perdrait ici la propriété `message` si on ne la déversait pas dans le nouvel état.
@ -103,20 +97,13 @@ Ensuite, on va définir notre état initial :
{% tabs defaultSelectedTab="js" %}
{% tab value="js" label="JavaScript" %}
```js
const initialState = { count: 0 };
```
{% snippet path="react/reducer/reducer-initial-state.js" language="js" /%}
{% /tab %}
{% tab value="ts" label="TypeScript" %}
```ts
type State = {
count: number;
};
const initialState: State = { count: 0 };
```
{% snippet path="react/reducer/reducer-initial-state.ts" language="ts" /%}
{% /tab %}
@ -128,52 +115,13 @@ On peut maintenant définir notre reducer :
{% tab value="js" label="JavaScript" %}
```js
const reducer = (state, action) => {
switch (action.type) {
case "INCREMENT":
return { ...state, count: state.count + 1 };
case "DECREMENT":
return { ...state, count: state.count - 1 };
case "RESET":
return { ...state, count: 0 };
case "SET":
return { ...state, count: action.payload };
default:
return state;
}
};
```
{% snippet path="react/reducer/reducer.js" language="js" showLineNumbers=true /%}
{% /tab %}
{% tab value="ts" label="TypeScript" %}
```ts
type State = {
count: number;
};
type Action = {
type: "INCREMENT" | "DECREMENT" | "RESET" | "SET";
payload?: number;
};
const reducer = (state: State, action: Action) => {
switch (action.type) {
case "INCREMENT":
return { ...state, count: state.count + 1 };
case "DECREMENT":
return { ...state, count: state.count - 1 };
case "RESET":
return { ...state, count: 0 };
case "SET":
return { ...state, count: action.payload! };
default:
return state;
}
};
```
{% snippet path="react/reducer/reducer.ts" language="ts" showLineNumbers=true /%}
{% /tab %}
@ -196,17 +144,13 @@ Enfin, on peut utiliser le hook useReducer dans notre composant :
{% tab value="js" label="JavaScript" %}
```js
const [state, dispatch] = useReducer(reducer, initialState);
```
{% snippet path="react/reducer/reducer-hook.js" language="js" /%}
{% /tab %}
{% tab value="ts" label="TypeScript" %}
```ts
const [state, dispatch] = useReducer<State, Action>(reducer, initialState);
```
{% snippet path="react/reducer/reducer-hook.ts" language="ts" /%}
{% /tab %}

View File

@ -0,0 +1 @@
const [state, dispatch] = useReducer(reducer, initialState);

View File

@ -0,0 +1 @@
const [state, dispatch] = useReducer<State, Action>(reducer, initialState);

View File

@ -0,0 +1 @@
const initialState = { count: 0 };

View File

@ -0,0 +1,4 @@
type State = {
count: number;
};
const initialState: State = { count: 0 };

View File

@ -0,0 +1,14 @@
const reducer = (state, action) => {
switch (action.type) {
case "INCREMENT":
return { ...state, count: state.count + 1 };
case "DECREMENT":
return { ...state, count: state.count - 1 };
case "RESET":
return { ...state, count: 0 };
case "SET":
return { ...state, count: action.payload };
default:
return state;
}
};

View File

@ -0,0 +1,23 @@
type State = {
count: number;
};
type Action = {
type: "INCREMENT" | "DECREMENT" | "RESET" | "SET";
payload?: number;
};
const reducer = (state: State, action: Action) => {
switch (action.type) {
case "INCREMENT":
return { ...state, count: state.count + 1 };
case "DECREMENT":
return { ...state, count: state.count - 1 };
case "RESET":
return { ...state, count: 0 };
case "SET":
return { ...state, count: action.payload! };
default:
return state;
}
};

View File

@ -10,6 +10,7 @@ export default tseslint.config(
{
ignores: [
"dist/*",
"data/snippets/*",
// Temporary compiled files
"**/*.ts.build-*.mjs",

View File

@ -20,6 +20,7 @@ type DocData = { title: string; description: string; content: string; sections:
type DocExtension = "mdx" | "md";
class DocsService {
private static readonly SNIPPETS_PATH = path.resolve(path.join(__dirname, "data", "snippets"));
private static readonly DOCS_PATH = path.resolve(path.join(__dirname, "data"));
private static readonly DOCS_EXTS: DocExtension[] = ["mdx", "md"]; // Order matters
private static instance: DocsService;
@ -83,18 +84,23 @@ class DocsService {
}
public fetchSnippets(content: string) {
try {
const identifierResults = snippetsService.identifyNewSnippets(content);
if (!identifierResults) return;
if (!identifierResults) return [];
const [snippetsToFetch, allSnippets] = identifierResults;
for (const snippet of snippetsToFetch) {
const absolutePath = path.resolve(__dirname, snippet);
const absolutePath = path.resolve(DocsService.SNIPPETS_PATH, snippet);
const content = fs.readFileSync(absolutePath, "utf-8");
snippetsService.setToCache(snippet, content);
}
return allSnippets;
return allSnippets || [];
} catch (error) {
console.error("Error fetching snippets:", error);
return [];
}
}
public async fetchDocs() {
@ -109,15 +115,13 @@ class DocsService {
.replace(`.${extension}`, "")
.replace(/\/$/g, "");
const allSnippets = this.fetchSnippets(content);
const ast = Markdoc.parse(content);
const title = ast.attributes?.frontmatter?.match(/^title:\s*(.*?)\s*$/m)?.[1];
const description = ast.attributes?.frontmatter?.match(/^description:\s*(.*?)\s*$/m)?.[1]?.replaceAll('"', "");
const sections: DocSection[] = [[title, null, []]];
this.extractSections(ast, sections);
this.setToCache(key, { title, description, content, sections, snippets: allSnippets || [] });
this.setToCache(key, { title, description, content, sections, snippets: this.fetchSnippets(content) });
return { key, sections };
}),
@ -152,6 +156,7 @@ class DocsService {
return doc;
} catch (error) {
console.error("Error fetching docs:", error);
return null;
}
}

View File

@ -20,5 +20,5 @@
"@syntax/*": ["./components/syntax/*"]
}
},
"exclude": ["dist"]
"exclude": ["dist", "data/snippets"]
}