Skip to content

Commit

Permalink
improve react-router usage, forbid cross-feature imports and barrel f…
Browse files Browse the repository at this point in the history
…iles
  • Loading branch information
alan2207 committed May 18, 2024
1 parent 2b6a228 commit bd85984
Show file tree
Hide file tree
Showing 53 changed files with 513 additions and 494 deletions.
44 changes: 39 additions & 5 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ module.exports = {
node: true,
es6: true,
},
parserOptions: { ecmaVersion: 8, sourceType: 'module' },
ignorePatterns: ['node_modules/*', 'public/mockServiceWorker.js', 'generators/*'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
ignorePatterns: [
'node_modules/*',
'public/mockServiceWorker.js',
'generators/*',
],
extends: ['eslint:recommended'],
overrides: [
{
Expand Down Expand Up @@ -38,10 +42,32 @@ module.exports = {
'plugin:vitest/legacy-recommended',
],
rules: {
'no-restricted-imports': [
// disables cross-feature imports:
'import/no-restricted-paths': [
'error',
{
patterns: ['@/features/*/*'],
zones: [
{
target: './src/features/comments',
from: './src/features',
except: ['./comments'],
},
{
target: './src/features/discussions',
from: './src/features',
except: ['./discussions'],
},
{
target: './src/features/teams',
from: './src/features',
except: ['./teams'],
},
{
target: './src/features/users',
from: './src/features',
except: ['./users'],
},
],
},
],
'import/no-cycle': 'error',
Expand All @@ -50,7 +76,15 @@ module.exports = {
'import/order': [
'error',
{
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object'],
groups: [
'builtin',
'external',
'internal',
'parent',
'sibling',
'index',
'object',
],
'newlines-between': 'always',
alphabetize: { order: 'asc', caseInsensitive: true },
},
Expand Down
70 changes: 28 additions & 42 deletions docs/project-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ src
|
+-- components # shared components used across the entire application
|
+-- config # all the global configuration, env variables etc. get exported from here and used in the app
+-- config # global configurations, exported env variables etc.
|
+-- features # feature based modules
|
Expand All @@ -19,7 +19,7 @@ src
|
+-- providers # all of the application providers
|
+-- routes # routes configuration
+-- routes # routes of the application
|
+-- stores # global state stores
|
Expand All @@ -45,57 +45,43 @@ src/features/awesome-feature
|
+-- hooks # hooks scoped to a specific feature
|
+-- routes # route components for a specific feature pages
|
+-- stores # state stores for a specific feature
|
+-- types # typescript types for TS specific feature domain
|
+-- utils # utility functions for a specific feature
|
+-- index.ts # entry point for the feature, it should serve as the public API of the given feature and exports everything that should be used outside the feature
```

Everything from a feature should be exported from the `index.ts` file which behaves as the public API of the feature.

You should import stuff from other features only by using:

`import {AwesomeComponent} from "@/features/awesome-feature"`

and not

`import {AwesomeComponent} from "@/features/awesome-feature/components/awesome-component`
Do not import across the features. Instead, compose different features at the application level in the `routes` folder. This way, you can ensure that each feature is independent and can be easily moved or removed without affecting other parts of the application.

This can also be configured in the ESLint configuration to disallow the later import by the following rule:
To forbid cross-feature imports, you can use ESLint:

```js
{
rules: {
'no-restricted-imports': [
'error',
'import/no-restricted-paths': [
'error',
{
zones: [
{
patterns: ['@/features/*/*'],
target: './src/features/comments',
from: './src/features',
except: ['./comments'],
},
{
target: './src/features/discussions',
from: './src/features',
except: ['./discussions'],
},
{
target: './src/features/teams',
from: './src/features',
except: ['./teams'],
},
{
target: './src/features/users',
from: './src/features',
except: ['./users'],
},
],

// ...rest of the configuration
}
```
To prevent circular dependencies, here is another ESLint rule that can be used:
```js
{
rules: {
'import/no-cycle': 'error',
// ...rest of the configuration
}
},
],
```
This was inspired by how [NX](https://nx.dev/) handles libraries that are isolated but available to be used by the other modules. Think of a feature as a library or a module that is self-contained but can expose different parts to other features via its entry point. This approach would also make it easier to split the application in a monorepo in the future.
This way, you can ensure that the codebase is clean and easy to maintain.
If you are still getting circular dependencies, consider breaking the feature into smaller features or moving the shared code to the `lib` folder. Or you can always opt-out of the barrel export pattern and import the files directly.
Sometimes, it will make more sense to move the whole API layer to the `lib` folder, especially if it is shared across multiple features. The same goes for the `stores` folder. If you have a global store that is used across the entire application, it should be moved to the `stores` folder.
6 changes: 2 additions & 4 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,17 @@ If you are already using `react-query`, you can use [react-query-auth](https://g

User information should be treated as a central piece of data accessible throughout the application. If you are already using `react-query`, consider using it for storing user data as well. Alternatively, you can leverage React context with hooks or opt for a third-party state management library to efficiently manage user state across your application.

[Auth Configuration Example Code](../src/features/auth/lib/auth.tsx)
[Auth Configuration Example Code](../src/lib/auth.tsx)

The application will assume the user is authenticated if a user object is present.

[Authenticated Route Protection Example Code](../src/features/auth/lib/protected-route.tsx)

### Authorization

Authorization is the process of verifying whether a user has permission to access a specific resource within the application.

#### RBAC (Role based access control)

[Authorization Configuration Example Code](../src/features/auth/lib/authorization.tsx)
[Authorization Configuration Example Code](../src/lib/authorization.tsx)

In a role-based authorization model, access to resources is determined by defining specific roles and associating them with permissions. For example, roles such as `USER` and `ADMIN` can be assigned different levels of access rights within the application. Users are then granted access based on their roles; for instance, restricting certain functionalities to regular users while permitting administrators to access all features and functionalities.

Expand Down
6 changes: 4 additions & 2 deletions src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { RouterProvider } from 'react-router-dom';

import { AppProvider } from '@/providers/app';
import { AppRoutes } from '@/routes';
import { router } from '@/routes';

function App() {
return (
<AppProvider>
<AppRoutes />
<RouterProvider router={router} />
</AppProvider>
);
}
Expand Down
File renamed without changes.
25 changes: 13 additions & 12 deletions src/components/layouts/dashboard-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { NavLink, useNavigate } from 'react-router-dom';
import logo from '@/assets/logo.svg';
import { Button } from '@/components/ui/button';
import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer';
import { ROLES, useAuthorization, useLogout } from '@/features/auth';
import { useLogout } from '@/lib/auth';
import { ROLES, useAuthorization } from '@/lib/authorization';
import { cn } from '@/utils/cn';

import {
Expand All @@ -22,6 +23,17 @@ type SideNavigationItem = {
icon: (props: React.SVGProps<SVGSVGElement>) => JSX.Element;
};

const Logo = () => {
return (
<Link className="flex items-center text-white" to="/">
<img className="h-8 w-auto" src={logo} alt="Workflow" />
<span className="text-sm font-semibold text-white">
Bulletproof React
</span>
</Link>
);
};

export function DashboardLayout({ children }: { children: React.ReactNode }) {
const logout = useLogout();
const { checkAccess } = useAuthorization();
Expand All @@ -36,17 +48,6 @@ export function DashboardLayout({ children }: { children: React.ReactNode }) {
},
].filter(Boolean) as SideNavigationItem[];

const Logo = () => {
return (
<Link className="flex items-center text-white" to="/">
<img className="h-8 w-auto" src={logo} alt="Workflow" />
<span className="text-sm font-semibold text-white">
Bulletproof React
</span>
</Link>
);
};

return (
<div className="flex min-h-screen w-full flex-col bg-muted/40">
<aside className="fixed inset-y-0 left-0 z-10 hidden w-60 flex-col border-r bg-black sm:flex">
Expand Down
7 changes: 0 additions & 7 deletions src/features/auth/api/get-user.ts

This file was deleted.

18 changes: 0 additions & 18 deletions src/features/auth/api/login.ts

This file was deleted.

5 changes: 0 additions & 5 deletions src/features/auth/api/logout.ts

This file was deleted.

34 changes: 0 additions & 34 deletions src/features/auth/api/register.ts

This file was deleted.

4 changes: 1 addition & 3 deletions src/features/auth/components/login-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import { Link, useSearchParams } from 'react-router-dom';

import { Button } from '@/components/ui/button';
import { Form, Input } from '@/components/ui/form';

import { loginInputSchema } from '../api/login';
import { useLogin } from '../lib/auth';
import { useLogin, loginInputSchema } from '@/lib/auth';

type LoginFormProps = {
onSuccess: () => void;
Expand Down
6 changes: 2 additions & 4 deletions src/features/auth/components/register-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ import { Link, useSearchParams } from 'react-router-dom';

import { Button } from '@/components/ui/button';
import { Form, Input, Select, Label, Switch } from '@/components/ui/form';
import { useTeams } from '@/features/teams';

import { registerInputSchema } from '../api/register';
import { useRegister } from '../lib/auth';
import { useTeams } from '@/features/teams/api/get-teams';
import { useRegister, registerInputSchema } from '@/lib/auth';

type RegisterFormProps = {
onSuccess: () => void;
Expand Down
8 changes: 0 additions & 8 deletions src/features/auth/index.ts

This file was deleted.

43 changes: 0 additions & 43 deletions src/features/auth/lib/auth.tsx

This file was deleted.

Loading

0 comments on commit bd85984

Please sign in to comment.