mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
@@ -32,7 +32,7 @@ yarn electron # Run Electron app
|
||||
|
||||
- TypeScript strict mode
|
||||
- Prettier for formatting (runs on pre-commit)
|
||||
- Follow DRY principle—extract reusable components
|
||||
- **DRY principle**: Never repeat UI elements across views—extract them into reusable components in `src/components/`. Same applies to logic—extract into custom hooks in `src/hooks/`
|
||||
|
||||
## React Patterns (Critical)
|
||||
|
||||
@@ -78,22 +78,41 @@ src/
|
||||
└── data/ # Static data (default subplebbits, etc.)
|
||||
```
|
||||
|
||||
## Documentation
|
||||
## Recommended Skills
|
||||
|
||||
The following docs exist for deeper guidance. **Do not read them automatically**—they are large and will bloat the context window. Instead:
|
||||
- Be aware they exist
|
||||
- Consult them when relevant to the task or when the user asks
|
||||
- Offer to read them if the user seems to need React pattern guidance
|
||||
Skills are more efficient than docs—they inject targeted guidance without bloating the context window.
|
||||
|
||||
Available docs:
|
||||
- **[docs/react-guide.md](docs/react-guide.md)** — Bad vs good React patterns with code examples
|
||||
- **[docs/you-might-not-need-an-effect.md](docs/you-might-not-need-an-effect.md)** — When to avoid useEffect (comprehensive)
|
||||
### Context7 (for library docs)
|
||||
|
||||
When you need documentation for libraries like **plebbit-react-hooks** or **plebbit-js**, use the Context7 skill to fetch current docs instead of relying on potentially outdated training data.
|
||||
|
||||
```bash
|
||||
npx skills add https://github.com/intellectronica/agent-skills --skill context7
|
||||
```
|
||||
|
||||
### Vercel React Best Practices
|
||||
|
||||
For deeper React/Next.js performance guidance. Provides 57 prioritized rules across 8 categories (waterfalls, bundle size, server-side performance, client-side fetching, re-renders, rendering, JS performance, and advanced patterns).
|
||||
|
||||
```bash
|
||||
npx skills add https://github.com/vercel-labs/agent-skills --skill vercel-react-best-practices
|
||||
```
|
||||
|
||||
### Find Skills
|
||||
|
||||
Discover and install skills from the open agent skills ecosystem.
|
||||
|
||||
```bash
|
||||
npx skills add https://github.com/vercel-labs/skills --skill find-skills
|
||||
```
|
||||
|
||||
## Recommended MCP Servers
|
||||
|
||||
If you need to look up library documentation (like plebbit-react-hooks or plebbit-js), suggest the user install the **Exa MCP server**. Exa's `get_code_context_exa` tool provides accurate, up-to-date docs and code context—it offers broader coverage and fewer hallucinations than alternatives like context7.
|
||||
### GitHub MCP
|
||||
|
||||
If you need to check Dependabot security alerts, read GitHub Actions logs, search issues/PRs, or look up code across GitHub, suggest the user install the **GitHub MCP server** with the `default,dependabot,actions` toolsets enabled.
|
||||
For Dependabot security alerts, GitHub Actions logs, issue/PR searches, or cross-repo code lookup, use the **GitHub MCP server** (with `default,dependabot,actions` toolsets enabled).
|
||||
|
||||
If not available, suggest the user install it.
|
||||
|
||||
### Context Window Warning
|
||||
|
||||
@@ -148,11 +167,26 @@ node scripts/update-translations.js --audit --dry
|
||||
node scripts/update-translations.js --audit --write
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### GitHub Issues
|
||||
|
||||
When proposing or implementing code changes, always suggest a GitHub issue title and description. Format:
|
||||
|
||||
- **Title**: Use [Conventional Commits](https://www.conventionalcommits.org/) style (e.g., `fix: ...`, `feat: ...`, `perf: ...`, `refactor: ...`, `docs: ...`, `chore: ...`)
|
||||
- **Description**: 2-3 informal sentences describing the problem (not the solution). Write as if the issue hasn't been fixed yet. Use markdown.
|
||||
|
||||
Use the `perf` type for performance optimizations (not `fix`).
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
When stuck on a bug or issue, search the web for solutions. Developer communities often have recent fixes or workarounds that aren't in training data.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Never commit secrets or API keys
|
||||
- Use yarn, not npm
|
||||
- Keep components focused—split large components
|
||||
- Add comments for complex logic, skip obvious code
|
||||
- Add comments for complex/unclear code (especially custom functions in this FOSS project with many contributors). Skip comments for obvious code
|
||||
- Test on mobile viewport (this is a responsive app)
|
||||
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
## [0.6.2](https://github.com/plebbit/plebchan/compare/v0.6.1...v0.6.2) (2026-01-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ci:** use setup-python action for macos release build ([3335f6f](https://github.com/plebbit/plebchan/commit/3335f6ff7675d93bd592b7390ba575fe7358eda3))
|
||||
* **post:** include pending replies in useReplies calls ([2887347](https://github.com/plebbit/plebchan/commit/28873478a29c54b7c1ec22f02ca7300d545b20a1))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **mod-queue:** improve compact view and add pending approval features ([fa4ec6d](https://github.com/plebbit/plebchan/commit/fa4ec6da40b42b7041774d747531c64dc10b26f6))
|
||||
|
||||
|
||||
|
||||
## [0.6.1](https://github.com/plebbit/plebchan/compare/v0.6.0...v0.6.1) (2026-01-25)
|
||||
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ _Telegram group for this repo https://t.me/fivechandev_
|
||||
|
||||
# 5chan
|
||||
|
||||
5chan is a serverless, adminless, decentralized and open-source 4chan alternative built on the [Plebbit protocol](https://plebbit.com). It features the same directory structure as 4chan, but with a crucial difference: **anyone can create and own boards, and multiple boards can compete for each directory slot**.
|
||||
5chan is a serverless, adminless, decentralized and open-source 4chan alternative built on the [Bitsocial protocol](https://bitsocial.net). It features the same directory structure as 4chan, but with a crucial difference: **anyone can create and own boards, and multiple boards can compete for each directory slot**.
|
||||
|
||||
## Key Features
|
||||
|
||||
### Decentralized Board Ownership
|
||||
Unlike traditional imageboards, 5chan has no global admins or central authority. Anyone can create unlimited boards using their own [plebbit node](https://github.com/plebbit/plebbit-cli). Each board owner runs their own P2P node that users connect to peer-to-peer, giving them complete control over their board's content, moderation, and rules.
|
||||
Unlike traditional imageboards, 5chan has no global admins or central authority. Anyone can create unlimited boards using their own [bitsocial node](https://github.com/bitsocialhq/bitsocial-cli). Each board owner runs their own P2P node that users connect to peer-to-peer, giving them complete control over their board's content, moderation, and rules.
|
||||
|
||||
### Competitive Directory System
|
||||
5chan maintains the familiar 4chan directory structure (Japanese Culture, Video Games, Interests, Creative, etc.), but introduces competition: **multiple boards can compete for each directory slot**. For example, there can be unlimited "Business & Finance" boards, but only the highest-voted one appears in the directory on the homepage.
|
||||
@@ -23,7 +23,7 @@ Currently, directory assignments are temporarily handpicked by developers throug
|
||||
|
||||
### How It Works
|
||||
|
||||
- **Current System**: Developers manually curate directory assignments by reviewing pull requests to the [5chan-multisub.json](https://github.com/plebbit/lists/blob/master/5chan-multisub.json) file. This is temporary until DAO curation is implemented.
|
||||
- **Current System**: Developers manually curate directory assignments by reviewing pull requests to the [5chan-multisub.json](https://github.com/bitsocialhq/lists/blob/master/5chan-multisub.json) file.
|
||||
|
||||
- **Future System**: Directory board assignments will be determined through gasless voting using pubsub. Community members will vote on which board should be assigned to each directory, and the highest-voted board will automatically become the directory board. This creates a competitive marketplace where board quality and community engagement determine directory placement.
|
||||
|
||||
@@ -33,33 +33,33 @@ Currently, directory assignments are temporarily handpicked by developers throug
|
||||
|
||||
#### In-App Board Creation
|
||||
|
||||
Creating boards directly from the 5chan web app (5chan.app) is planned. This requires connecting via RPC to a plebbit node—technically already possible, but there's no default connection configured. A default connection would require a public RPC service (similar to what Infura provides for crypto wallets, but for plebbit nodes). This would allow all users to be connected to a P2P node by default using a free tier subscription in the background, potentially monetized via ads injected in the RPC service-owned boards.
|
||||
Creating boards directly from the 5chan web app (5chan.app) is planned. This requires connecting via RPC to a bitsocial node—technically already possible, but there's no default connection configured. A default connection would require a public RPC service (similar to what Infura provides for crypto wallets, but for bitsocial nodes). This would allow all users to be connected to a P2P node by default using a free tier subscription in the background, potentially monetized via ads injected in the RPC service-owned boards.
|
||||
|
||||
#### Pubsub Voting
|
||||
|
||||
The protocol design for pubsub voting is already drafted in [plebbit-js issue #25](https://github.com/plebbit/plebbit-js/issues/25). This will enable:
|
||||
The protocol design for pubsub voting is already drafted in [pkc-js issue #25](https://github.com/pkcprotocol/pkc-js/issues/25). This will enable:
|
||||
- Gasless voting using pubsub topics
|
||||
- Weighted voting based on token balances
|
||||
- Automatic directory resolution based on vote tallies
|
||||
- Full decentralization without any intermediaries
|
||||
|
||||
This feature is on the plebbit-js roadmap but hasn't been implemented yet.
|
||||
This feature is on the pkc-js roadmap but hasn't been implemented yet.
|
||||
|
||||
## Downloads
|
||||
|
||||
- **Web version**: https://5chan.app (also available using Brave/IPFS Companion on https://5chan.eth)
|
||||
- **Desktop version** (full P2P plebbit node, seeds automatically): Available for Mac/Windows/Linux, [download from the release page](https://github.com/plebbit/5chan/releases/latest)
|
||||
- **Mobile version**: Available for Android, [download from the release page](https://github.com/plebbit/5chan/releases/latest)
|
||||
- **Desktop version** (full P2P bitsocial node, seeds automatically): Available for Mac/Windows/Linux, [download from the release page](https://github.com/bitsocialhq/5chan/releases/latest)
|
||||
- **Mobile version**: Available for Android, [download from the release page](https://github.com/bitsocialhq/5chan/releases/latest)
|
||||
|
||||
## Creating a Board
|
||||
|
||||
In the plebbit protocol, a 5chan board is called a _subplebbit_. To create and run a subplebbit:
|
||||
In the bitsocial protocol, a 5chan board is called a _community_. To create and run a community:
|
||||
|
||||
1. Install plebbit-cli, available for Windows, macOS, and Linux: [latest release](https://github.com/plebbit/plebbit-cli/releases/latest)
|
||||
2. Follow the instructions in the repo's README
|
||||
3. When running the daemon for the first time, it will output WebUI links you can use to manage your subplebbit with a GUI
|
||||
1. Install bitsocial-cli, available for Windows, macOS, and Linux: [latest release](https://github.com/bitsocialhq/bitsocial-cli/releases/latest);
|
||||
2. Follow the instructions in the repo's README;
|
||||
3. When running the daemon for the first time, it will output WebUI links you can use to manage your bitsocial community with a GUI.
|
||||
|
||||
Once created, anyone can connect to your subplebbit using any plebbit client (such as 5chan) by using the subplebbit's address. The address is not stored in any central database—plebbit is a pure peer-to-peer protocol.
|
||||
Once created, anyone can connect to your community using any bitsocial client (such as 5chan) by using the community address. The address is not stored in any central database—bitsocial is a pure peer-to-peer protocol.
|
||||
|
||||
**Note**: Creating boards directly from the 5chan web app is planned for the future (see [Future Roadmap](#future-roadmap)).
|
||||
|
||||
@@ -72,16 +72,16 @@ To have your board appear in a directory on the 5chan homepage:
|
||||
- Relevant to the directory category
|
||||
- **99% uptime** (since a board acts like its own server—it's a P2P node)
|
||||
|
||||
2. Open a pull request on GitHub by editing the [5chan-multisub.json](https://github.com/plebbit/lists/blob/master/5chan-multisub.json) file
|
||||
2. Open a pull request on GitHub by editing the [5chan-multisub.json](https://github.com/bitsocialhq/lists/blob/master/5chan-multisub.json) file
|
||||
|
||||
3. Add your board's entry with:
|
||||
- Title
|
||||
- Address
|
||||
- NSFW status (if applicable)
|
||||
- Title: in the format `/directoryCode/ - Title`, e.g. "/biz/ - Business & Finance";
|
||||
- Address: the bitsocial community address, whether IPNS key (`12KooW...`) or readable crypto address (`mydomain.eth`);
|
||||
- NSFW status: `true` or `false`, must be the same as 4chan.
|
||||
|
||||
4. The developers will review your PR and merge it if approved
|
||||
|
||||
**Note**: Even if your board isn't assigned to a directory, users can still access it at any time using its address. Directory assignment only affects visibility on the homepage.
|
||||
**Note**: Even if your board isn't assigned to a directory, users can still access it at any time using its bitsocial community address. Directory assignment only affects visibility on the homepage.
|
||||
|
||||
## Development
|
||||
|
||||
@@ -106,7 +106,7 @@ To have your board appear in a directory on the 5chan homepage:
|
||||
|
||||
### Challenge Types
|
||||
|
||||
Subplebbits can require challengers to solve one or more prompts before a publication is accepted. 5chan already supported text and image prompts, and now also handles `url/iframe` challenges so Mintpass communities can run their iframe flow directly inside the modal. The modal first shows a hostname confirmation (showing only the host for mintpass.org, full URL otherwise), then opens the HTTPS iframe with the current theme, replaces `{userAddress}` tokens with the signed-in address, and submits automatically when the user finishes.
|
||||
Bitsocial communities can require users to solve one or more anti-spam challenges before a publication is accepted. 5chan already supports multiple challenge types, including `url/iframe` challenges so [Mintpass](https://github.com/bitsocialhq/mintpass) communities can run their iframe flow directly inside a modal. The modal first shows a hostname confirmation (showing only the host for mintpass.org, full URL otherwise), then opens the HTTPS iframe with the current theme, replaces `{userAddress}` tokens with the signed-in address, and submits automatically when the user finishes.
|
||||
|
||||
### Build
|
||||
|
||||
|
||||
@@ -1,813 +0,0 @@
|
||||
# React guide
|
||||
|
||||
A lot of AI-generated React code is heavily biased toward using useState and useEffect, often in verbose or naive ways. That’s because these hooks are:
|
||||
|
||||
1. Explicit, making them easy for LLMs to pattern-match.
|
||||
2. Safe defaults, requiring less architectural decision-making.
|
||||
3. Well-documented, so the model’s training data is oversaturated with examples.
|
||||
|
||||
But modern, well-architected React apps rarely rely on raw useState/useEffect beyond simple UI state or lifecycle quirks. Here’s a breakdown of what serious devs use instead—and why this is the real moat for human engineers.
|
||||
|
||||
Alternatives to useState and useEffect
|
||||
|
||||
1. State Machines / Statecharts (e.g. Zustand)
|
||||
- Why: Replaces ad-hoc local state management with deterministic, declarative logic.
|
||||
- Advantage: Less bugs, easier to test, clearer state transitions.
|
||||
- AI gap: LLMs struggle to model state transitions declaratively. LLM Agent must overcome this.
|
||||
|
||||
2. React Query / SWR for Data Fetching
|
||||
- Why: Avoids useEffect-based data fetching and loading/error state juggling.
|
||||
- Advantage: Caching, stale-while-revalidate, retries, pagination—all handled out of the box.
|
||||
- AI gap: AI often uses useEffect + fetch and forgets cleanup, race conditions, or caching.
|
||||
|
||||
3. Global State with Zustand, Jotai, Recoil, or Redux Toolkit
|
||||
- Why: Local useState doesn’t scale for shared or persistent state.
|
||||
- Advantage: Better performance, devtools support, persistence.
|
||||
- AI gap: Defaults to prop-drilling or bloated context APIs.
|
||||
|
||||
4. Custom Hooks and Composition
|
||||
- Why: Reuse logic cleanly without bloating components or copy-pasting useEffect.
|
||||
- Advantage: Separation of concerns, encapsulation.
|
||||
- AI gap: Often fails to factor logic out, keeping everything in one component.
|
||||
|
||||
## Why This Matters
|
||||
|
||||
AI can mimic patterns, but it can’t:
|
||||
- Architect a system for long-term maintainability.
|
||||
- Predict runtime performance tradeoffs.
|
||||
- Refactor spaghetti effects into custom hooks or state machines.
|
||||
- Decide when state should be colocated vs globalized.
|
||||
- Avoid footguns like stale closures, unnecessary re-renders, or race conditions.
|
||||
|
||||
In short: AI is great at writing code, but bad at engineering software.
|
||||
|
||||
Takeaway
|
||||
- Use fewer useEffect and useState hooks.
|
||||
- Learn to design systems, not just code components.
|
||||
- Reach for declarative, composable patterns.
|
||||
- Understand caching, reactivity, and scalability beyond local state.
|
||||
|
||||
Here’s a side-by-side breakdown of common AI-generated React patterns (bad) vs what experienced engineers write (good) across 4 key areas: state management, data fetching, side effects, and logic reuse.
|
||||
|
||||
## 1. State Management
|
||||
|
||||
❌ Bad (AI-style useState)
|
||||
|
||||
```typescript
|
||||
function TodoApp() {
|
||||
const [todos, setTodos] = useState([]);
|
||||
const [input, setInput] = useState('');
|
||||
|
||||
const addTodo = () => {
|
||||
setTodos([...todos, { text: input }]);
|
||||
setInput('');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<input value={input} onChange={(e) => setInput(e.target.value)} />
|
||||
<button onClick={addTodo}>Add</button>
|
||||
<ul>
|
||||
{todos.map((todo, i) => <li key={i}>{todo.text}</li>)}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
✅ Good (Zustand store, state extracted)
|
||||
|
||||
```typescript
|
||||
import { create } from 'zustand';
|
||||
|
||||
const useTodoStore = create((set) => ({
|
||||
todos: [],
|
||||
input: '',
|
||||
addTodo: () => set((state) => ({
|
||||
todos: [...state.todos, { text: state.input }],
|
||||
input: '',
|
||||
})),
|
||||
setInput: (val) => set({ input: val }),
|
||||
}));
|
||||
|
||||
function TodoApp() {
|
||||
const { todos, input, setInput, addTodo } = useTodoStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
<input value={input} onChange={(e) => setInput(e.target.value)} />
|
||||
<button onClick={addTodo}>Add</button>
|
||||
<ul>
|
||||
{todos.map((todo, i) => <li key={i}>{todo.text}</li>)}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 2. Data Fetching
|
||||
|
||||
❌ Bad (AI-style useEffect)
|
||||
|
||||
```typescript
|
||||
function UserList() {
|
||||
const [users, setUsers] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/users')
|
||||
.then(res => res.json())
|
||||
.then(setUsers);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{users.map(u => <li key={u.id}>{u.name}</li>)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
✅ Good (React Query)
|
||||
|
||||
```typescript
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
function UserList() {
|
||||
const { data: users = [], isLoading } = useQuery({
|
||||
queryKey: ['users'],
|
||||
queryFn: () => fetch('/api/users').then(res => res.json()),
|
||||
});
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{users.map(u => <li key={u.id}>{u.name}</li>)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Side Effects
|
||||
|
||||
❌ Bad (naive effect with race condition)
|
||||
|
||||
```typescript
|
||||
function Search({ term }) {
|
||||
const [results, setResults] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/search?q=${term}`)
|
||||
.then(res => res.json())
|
||||
.then(setResults);
|
||||
}, [term]);
|
||||
|
||||
return <div>{results.length} results</div>;
|
||||
}
|
||||
```
|
||||
|
||||
✅ Good (abort previous fetch with AbortController)
|
||||
|
||||
```typescript
|
||||
function Search({ term }) {
|
||||
const [results, setResults] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
|
||||
fetch(`/api/search?q=${term}`, { signal: controller.signal })
|
||||
.then(res => res.json())
|
||||
.then(setResults)
|
||||
.catch((e) => {
|
||||
if (e.name !== 'AbortError') console.error(e);
|
||||
});
|
||||
|
||||
return () => controller.abort();
|
||||
}, [term]);
|
||||
|
||||
return <div>{results.length} results</div>;
|
||||
}
|
||||
```
|
||||
|
||||
Better yet? Don’t use useEffect at all. Use React Query with term as a key.
|
||||
|
||||
## 4. Logic Reuse
|
||||
|
||||
❌ Bad (repeated logic inline)
|
||||
|
||||
```typescript
|
||||
function ComponentA() {
|
||||
const [count, setCount] = useState(0);
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setCount(c => c + 1), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
|
||||
function ComponentB() {
|
||||
const [count, setCount] = useState(0);
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setCount(c => c + 1), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
✅ Good (custom hook)
|
||||
|
||||
```typescript
|
||||
function useCounter(intervalMs = 1000) {
|
||||
const [count, setCount] = useState(0);
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setCount(c => c + 1), intervalMs);
|
||||
return () => clearInterval(id);
|
||||
}, [intervalMs]);
|
||||
return count;
|
||||
}
|
||||
|
||||
function ComponentA() {
|
||||
const count = useCounter();
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
|
||||
function ComponentB() {
|
||||
const count = useCounter();
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 5. State Machines with Zustand
|
||||
|
||||
❌ Bad (AI-style complex useState with boolean flags)
|
||||
|
||||
```typescript
|
||||
function OrderProcess() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isError, setIsError] = useState(false);
|
||||
const [isSuccess, setIsSuccess] = useState(false);
|
||||
const [cart, setCart] = useState([]);
|
||||
const [paymentDetails, setPaymentDetails] = useState(null);
|
||||
const [orderPlaced, setOrderPlaced] = useState(false);
|
||||
|
||||
const addToCart = (item) => {
|
||||
setCart([...cart, item]);
|
||||
};
|
||||
|
||||
const submitPayment = async (details) => {
|
||||
setIsLoading(true);
|
||||
setIsError(false);
|
||||
|
||||
try {
|
||||
await processPayment(details);
|
||||
setPaymentDetails(details);
|
||||
setIsSuccess(true);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
setIsError(true);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const placeOrder = async () => {
|
||||
setIsLoading(true);
|
||||
setIsError(false);
|
||||
|
||||
try {
|
||||
await submitOrder(cart, paymentDetails);
|
||||
setOrderPlaced(true);
|
||||
setIsSuccess(true);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
setIsError(true);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Complex conditional rendering based on multiple state variables */}
|
||||
{isLoading && <LoadingSpinner />}
|
||||
{isError && <ErrorMessage />}
|
||||
{!isLoading && !isError && !orderPlaced && (
|
||||
<>
|
||||
<CartItems items={cart} onAddItem={addToCart} />
|
||||
<PaymentForm onSubmit={submitPayment} />
|
||||
{paymentDetails && <OrderButton onClick={placeOrder} />}
|
||||
</>
|
||||
)}
|
||||
{orderPlaced && <OrderConfirmation />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
✅ Good (Zustand state machine)
|
||||
|
||||
```typescript
|
||||
import { create } from 'zustand';
|
||||
|
||||
// Create a Zustand store with state machine pattern
|
||||
const useOrderStore = create((set, get) => ({
|
||||
// State machine's current state
|
||||
state: 'browsing',
|
||||
|
||||
// Context/data
|
||||
cart: [],
|
||||
paymentDetails: null,
|
||||
error: null,
|
||||
|
||||
// Actions that transition between states
|
||||
addToCart: (item) => set((state) => ({
|
||||
cart: [...state.cart, item]
|
||||
})),
|
||||
|
||||
goToCheckout: () => set({ state: 'paymentEntry' }),
|
||||
|
||||
goBack: () => {
|
||||
const { state: currentState } = get();
|
||||
if (currentState === 'paymentEntry') {
|
||||
set({ state: 'browsing' });
|
||||
} else if (currentState === 'confirmOrder') {
|
||||
set({ state: 'paymentEntry' });
|
||||
}
|
||||
},
|
||||
|
||||
submitPayment: async (details) => {
|
||||
set({ state: 'processingPayment', error: null });
|
||||
|
||||
try {
|
||||
const result = await processPayment(details);
|
||||
set({
|
||||
state: 'confirmOrder',
|
||||
paymentDetails: result
|
||||
});
|
||||
} catch (error) {
|
||||
set({
|
||||
state: 'paymentEntry',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
placeOrder: async () => {
|
||||
const { cart, paymentDetails } = get();
|
||||
set({ state: 'processingOrder', error: null });
|
||||
|
||||
try {
|
||||
await submitOrder(cart, paymentDetails);
|
||||
set({ state: 'orderComplete' });
|
||||
} catch (error) {
|
||||
set({
|
||||
state: 'confirmOrder',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
function OrderProcess() {
|
||||
const {
|
||||
state,
|
||||
cart,
|
||||
paymentDetails,
|
||||
error,
|
||||
addToCart,
|
||||
goToCheckout,
|
||||
goBack,
|
||||
submitPayment,
|
||||
placeOrder
|
||||
} = useOrderStore();
|
||||
|
||||
// Render UI based on current state
|
||||
return (
|
||||
<div>
|
||||
{error && <ErrorMessage message={error} />}
|
||||
|
||||
{state === 'browsing' && (
|
||||
<CartItems
|
||||
items={cart}
|
||||
onAddItem={addToCart}
|
||||
onCheckout={goToCheckout}
|
||||
/>
|
||||
)}
|
||||
|
||||
{state === 'paymentEntry' && (
|
||||
<PaymentForm
|
||||
onSubmit={submitPayment}
|
||||
onBack={goBack}
|
||||
/>
|
||||
)}
|
||||
|
||||
{state === 'processingPayment' && (
|
||||
<LoadingSpinner message="Processing payment..." />
|
||||
)}
|
||||
|
||||
{state === 'confirmOrder' && (
|
||||
<OrderSummary
|
||||
cart={cart}
|
||||
paymentDetails={paymentDetails}
|
||||
onConfirm={placeOrder}
|
||||
onBack={goBack}
|
||||
/>
|
||||
)}
|
||||
|
||||
{state === 'processingOrder' && (
|
||||
<LoadingSpinner message="Placing your order..." />
|
||||
)}
|
||||
|
||||
{state === 'orderComplete' && (
|
||||
<OrderConfirmation orderDetails={{ cart, paymentDetails }} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Key Benefits of the Zustand State Machine Approach:
|
||||
|
||||
1. **Explicit states**: The system can only be in one well-defined state at a time
|
||||
2. **Centralized logic**: All state transitions are defined in one place
|
||||
3. **Predictable transitions**: State changes follow clear patterns
|
||||
4. **Simplified UI logic**: Components just render based on the current state
|
||||
5. **Async handling**: Asynchronous operations are contained within the state transitions
|
||||
6. **Shared state**: Any component can access the store without prop drilling
|
||||
7. **Improved debugging**: The current state is always clear and predictable
|
||||
8. **No impossible states**: Unlike boolean flags that could create invalid combinations
|
||||
9. **Simpler API**: Compared to XState, Zustand offers a more approachable API for many developers
|
||||
10. **Smaller bundle size**: Zustand is significantly smaller than XState
|
||||
|
||||
This approach gives most of the benefits of formal state machines while maintaining the simplicity and familiarity of React state management. It's a pragmatic middle ground that's often easier to adopt in existing projects.
|
||||
|
||||
## 6. Concurrent UI Techniques
|
||||
|
||||
❌ Bad (Blocking UI updates without concurrency):
|
||||
```jsx
|
||||
// Bad example: Synchronously fetching data in useEffect can cause UI jank.
|
||||
function SearchResults({ query }) {
|
||||
const [results, setResults] = React.useState([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
fetch(`/api/search?q=${query}`)
|
||||
.then(res => res.json())
|
||||
.then(data => setResults(data));
|
||||
}, [query]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{results.map(item => <p key={item.id}>{item.title}</p>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
✅ Good (Using useTransition for non-blocking updates):
|
||||
```jsx
|
||||
// Good example: Leveraging useTransition to mark state updates as low-priority.
|
||||
function SearchResults({ query }) {
|
||||
const [results, setResults] = React.useState([]);
|
||||
const [isPending, startTransition] = React.useTransition();
|
||||
|
||||
React.useEffect(() => {
|
||||
startTransition(() => {
|
||||
fetch(`/api/search?q=${query}`)
|
||||
.then(res => res.json())
|
||||
.then(data => setResults(data));
|
||||
});
|
||||
}, [query, startTransition]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isPending && <div>Loading...</div>}
|
||||
{results.map(item => <p key={item.id}>{item.title}</p>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Routing and Navigation (Not applicable with Next.js apps)
|
||||
|
||||
❌ Bad (Manual routing without a dedicated library):
|
||||
```jsx
|
||||
// Bad example: Handling routing manually using state and window history.
|
||||
function App() {
|
||||
const [route, setRoute] = React.useState(window.location.pathname);
|
||||
|
||||
React.useEffect(() => {
|
||||
const onPopState = () => setRoute(window.location.pathname);
|
||||
window.addEventListener('popstate', onPopState);
|
||||
return () => window.removeEventListener('popstate', onPopState);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{route === '/' && <Home />}
|
||||
{route === '/about' && <About />}
|
||||
<button onClick={() => {
|
||||
window.history.pushState({}, '', '/about');
|
||||
setRoute('/about');
|
||||
}}>
|
||||
About
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
✅ Good (Using React Router v6 with lazy-loaded routes):
|
||||
```jsx
|
||||
// Good example: Utilizing React Router to manage routes and lazy load components.
|
||||
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
|
||||
const Home = lazy(() => import('./Home'));
|
||||
const About = lazy(() => import('./About'));
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<nav>
|
||||
<Link to="/">Home</Link>
|
||||
<Link to="/about">About</Link>
|
||||
</nav>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/about" element={<About />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 8. Error Handling
|
||||
|
||||
❌ Bad (No error boundary; crashes on errors):
|
||||
```jsx
|
||||
// Bad example: A component that throws errors without an error boundary.
|
||||
function MyComponent({ data }) {
|
||||
if (!data) {
|
||||
throw new Error("Data not found");
|
||||
}
|
||||
return <div>{data}</div>;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return <MyComponent data={null} />;
|
||||
}
|
||||
```
|
||||
|
||||
✅ Good (Wrapping components with an Error Boundary):
|
||||
```jsx
|
||||
// Good example: Using an ErrorBoundary component to catch and handle errors gracefully.
|
||||
import React from 'react';
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
console.error("Error caught in boundary:", error, errorInfo);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return <h1>Something went wrong.</h1>;
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
function MyComponent({ data }) {
|
||||
if (!data) {
|
||||
throw new Error("Data not found");
|
||||
}
|
||||
return <div>{data}</div>;
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<MyComponent data={null} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 9. Form Management
|
||||
|
||||
❌ Bad (Manual state handling with no validation):
|
||||
```jsx
|
||||
// Bad example: Uncontrolled form without validation; error handling is omitted.
|
||||
function ContactForm() {
|
||||
const [formData, setFormData] = React.useState({ name: '', email: '' });
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
console.log(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="Name"
|
||||
/>
|
||||
<input
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
placeholder="Email"
|
||||
/>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
✅ Good (Using React Hook Form with Zod for schema validation):
|
||||
|
||||
```jsx
|
||||
import React from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
// 1. Define your schema with Zod
|
||||
const contactSchema = z.object({
|
||||
name: z.string().nonempty('Name is required'),
|
||||
email: z
|
||||
.string()
|
||||
.nonempty('Email is required')
|
||||
.email('Invalid email'),
|
||||
});
|
||||
|
||||
// 2. Infer the TypeScript type of your form data
|
||||
type ContactFormData = z.infer<typeof contactSchema>;
|
||||
|
||||
export function ContactForm() {
|
||||
// 3. Hook up RHF with the zodResolver
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<ContactFormData>({
|
||||
resolver: zodResolver(contactSchema),
|
||||
});
|
||||
|
||||
const onSubmit = (data: ContactFormData) => {
|
||||
console.log(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div>
|
||||
<input
|
||||
{...register('name')}
|
||||
placeholder="Name"
|
||||
aria-invalid={!!errors.name}
|
||||
/>
|
||||
{errors.name && <p role="alert">{errors.name.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
{...register('email')}
|
||||
placeholder="Email"
|
||||
aria-invalid={!!errors.email}
|
||||
/>
|
||||
{errors.email && <p role="alert">{errors.email.message}</p>}
|
||||
</div>
|
||||
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 10. Code Splitting and Lazy Loading
|
||||
|
||||
❌ Bad (Eagerly importing heavy components, increasing the initial bundle size):
|
||||
```jsx
|
||||
// Bad example: Directly importing a large component causing a heavier bundle.
|
||||
import HeavyComponent from './HeavyComponent';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<HeavyComponent />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
✅ Good (Lazy loading heavy components with React.lazy and Suspense):
|
||||
```jsx
|
||||
// Good example: Dynamically importing components to reduce initial load time.
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
|
||||
const HeavyComponent = lazy(() => import('./HeavyComponent'));
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Suspense fallback={<div>Loading Component...</div>}>
|
||||
<HeavyComponent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 11. Testing Strategies
|
||||
|
||||
❌ Bad (Testing implementation details, resulting in brittle tests):
|
||||
```jsx
|
||||
// Bad example: Relying on internal DOM structure which may change.
|
||||
import { render } from '@testing-library/react';
|
||||
import MyComponent from './MyComponent';
|
||||
|
||||
test('MyComponent renders data', () => {
|
||||
const { container } = render(<MyComponent />);
|
||||
expect(container.querySelector('.data')).toBeDefined();
|
||||
});
|
||||
```
|
||||
|
||||
✅ Good (Focusing on user-centric behaviors using React Testing Library):
|
||||
```jsx
|
||||
// Good example: Testing component behavior by simulating user interactions.
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import MyComponent from './MyComponent';
|
||||
|
||||
test('MyComponent displays loaded data after user action', async () => {
|
||||
render(<MyComponent />);
|
||||
|
||||
// Check for an initial loading state
|
||||
expect(screen.getByText(/Loading/i)).toBeInTheDocument();
|
||||
|
||||
// Simulate user action that triggers data loading
|
||||
const button = screen.getByRole('button', { name: /load data/i });
|
||||
userEvent.click(button);
|
||||
|
||||
// Wait and assert that data is displayed
|
||||
expect(await screen.findByText(/Data loaded/i)).toBeInTheDocument();
|
||||
});
|
||||
```
|
||||
|
||||
## 12. Accessibility & Theming
|
||||
|
||||
❌ Bad (Non-semantic elements and missing ARIA labels):
|
||||
```jsx
|
||||
// Bad example: Using a div as a clickable element without semantics or accessibility.
|
||||
function ThemedButton() {
|
||||
return <div onClick={() => console.log('Clicked!')}>Click me</div>;
|
||||
}
|
||||
```
|
||||
|
||||
✅ Good (Semantic button with ARIA attributes and theming via CSS variables):
|
||||
```jsx
|
||||
// Good example: Accessible button component that utilizes theming.
|
||||
function ThemedButton() {
|
||||
return (
|
||||
<button
|
||||
aria-label="Click me"
|
||||
onClick={() => console.log('Clicked!')}
|
||||
style={{ padding: '8px 16px', backgroundColor: 'var(--primary-color)', color: '#fff' }}
|
||||
>
|
||||
Click me
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/* CSS (in a separate file or styled-components):
|
||||
:root {
|
||||
--primary-color: #007bff;
|
||||
}
|
||||
*/
|
||||
```
|
||||
|
||||
# Summary Table
|
||||
|
||||
| Concern | Bad AI Pattern | Solid Engineering Practice |
|
||||
|---------|---------------|----------------------------|
|
||||
| **State Management** | `useState` everywhere, prop‑drilling | Central store (Zustand / Redux Toolkit) or colocated custom hooks |
|
||||
| **Data Fetching** | `useEffect` + `fetch`, manual loading/error juggling | React Query / SWR with caching, retries, stale‑while‑revalidate |
|
||||
| **Side Effects** | Effects without cleanup → race conditions, leaks | AbortController or query library handles cancellation & retries |
|
||||
| **Logic Reuse** | Copy‑pasted hooks and state in every component | Extract cross‑cutting logic into custom hooks |
|
||||
| **Workflow / State Machine** | Boolean‑flag soup, impossible states | Deterministic statechart in a single store (Zustand / XState) |
|
||||
| **Concurrent UI Updates** | Blocking synchronous work inside effects | `useTransition` / `startTransition` for low‑priority updates |
|
||||
| **Routing & Navigation** | DIY history manipulation, inline route state | React Router v6 (or Next.js built‑ins) with lazy‑loaded routes |
|
||||
| **Error Handling** | No error boundary → full‑app crashes | Top‑level `ErrorBoundary` plus logging in `componentDidCatch` |
|
||||
| **Form Management** | Manual state, zero validation or feedback | React Hook Form + Yup/Zod schema for declarative validation |
|
||||
| **Code Splitting & Lazy Loading** | Eager imports ballooning bundle size | `React.lazy` + `Suspense` for on‑demand loading |
|
||||
| **Testing Strategy** | Brittle tests keyed to DOM structure | User‑centric tests with React Testing Library & jest‑dom |
|
||||
| **Accessibility & Theming** | Non‑semantic elements, missing ARIA, inline colors | Semantic HTML, ARIA labels, CSS variables / theme provider |
|
||||
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "5chan",
|
||||
"version": "0.6.1",
|
||||
"version": "0.6.2",
|
||||
"description": "A bitsocial client with a 4chan UI",
|
||||
"type": "module",
|
||||
"author": "Bitsocial Labs",
|
||||
|
||||
Reference in New Issue
Block a user