Skip to content

Commit

Permalink
Merge pull request #160 from alan2207/dev
Browse files Browse the repository at this point in the history
More improvements
  • Loading branch information
alan2207 committed May 19, 2024
2 parents fc23c32 + 47d4255 commit 05992c4
Show file tree
Hide file tree
Showing 89 changed files with 961 additions and 696 deletions.
69 changes: 64 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,57 @@ module.exports = {
'plugin:vitest/legacy-recommended',
],
rules: {
'no-restricted-imports': [
'import/no-restricted-paths': [
'error',
{
patterns: ['@/features/*/*'],
zones: [
// disables cross-feature imports:
// eg. src/features/discussions should not import from src/features/comments, etc.
{
target: './src/features/auth',
from: './src/features',
except: ['./auth'],
},
{
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'],
},
// enforce unidirectional codebase:

// e.g. src/app can import from src/features but not the other way around
{
target: './src/features',
from: './src/app',
},

// e.g src/features and src/app can import from these shared modules but not the other way around
{
target: [
'./src/components',
'./src/hooks',
'./src/lib',
'./src/types',
'./src/utils',
],
from: ['./src/features', './src/app'],
},
],
},
],
'import/no-cycle': 'error',
Expand All @@ -50,7 +101,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
Binary file added docs/assets/unidirectional-codebase.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
117 changes: 79 additions & 38 deletions docs/project-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,23 @@ Most of the code lives in the `src` folder and looks something like this:
```sh
src
|
+-- app # application layer containing:
| |
| +-- routes # application routes / can also be called pages
+-- app.tsx # main application component
+-- app-provider # application provider that wraps the entire application with global providers
+-- assets # assets folder can contain all the static files such as images, fonts, etc.
|
+-- 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
|
+-- hooks # shared hooks used across the entire application
|
+-- lib # reusable libraries preconfigured for the application
|
+-- providers # all of the application providers
|
+-- routes # routes configuration
|
+-- stores # global state stores
|
+-- test # test utilities and mocks
Expand All @@ -45,57 +46,97 @@ 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
+-- types # typescript types used within the feature
|
+-- 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:
NOTE: You don't need all of these folders for every feature. Only include the ones that are necessary for the feature.

`import {AwesomeComponent} from "@/features/awesome-feature"`
In the past, it was recommended to use barrel files to export all the files from a feature. However, it can cause issues for Vite to do tree shaking and can lead to performance issues. Therefore, it is recommended to import the files directly.

and not
It might be not be a good idea to import across the features. Instead, compose different features at the application level. This way, you can ensure that each feature is independent which makes the codebase less convoluted.

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

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: [
// disables cross-feature imports:
// eg. src/features/discussions should not import from src/features/comments, etc.
{
patterns: ['@/features/*/*'],
target: './src/features/auth',
from: './src/features',
except: ['./auth'],
},
{
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
}
// More restrictions...
],
},
],
```

To prevent circular dependencies, here is another ESLint rule that can be used:
You might also want to enforce unidirectional codebase architecture. This means that the code should flow in one direction, from shared parts of the code to the application (shared -> features -> app). This is a good practice to follow as it makes the codebase more predictable and easier to understand.

```js
{
rules: {
'import/no-cycle': 'error',
// ...rest of the configuration
}
```
![Unidirectional Codebase](./assets/unidirectional-codebase.png)

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.
As you can see, the shared parts can be used by any part of the codebase, but the features can only import from shared parts and the app can import from features and shared parts.

This way, you can ensure that the codebase is clean and easy to maintain.
To enforce this, you can use ESLint:

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.
```js
'import/no-restricted-paths': [
'error',
{
zones: [
// Previous restrictions...

// enforce unidirectional codebase:
// e.g. src/app can import from src/features but not the other way around
{
target: './src/features',
from: './src/app',
},

// e.g src/features and src/app can import from these shared modules but not the other way around
{
target: [
'./src/components',
'./src/hooks',
'./src/lib',
'./src/types',
'./src/utils',
],
from: ['./src/features', './src/app'],
},
],
},
],
```

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.
By following these practices, you can ensure that your codebase is well-organized, scalable, and maintainable. This will help you and your team to work more efficiently and effectively on the project.
This approach can also make it easier to apply similar architecture to apps built with Next.js, Remix or React Native.
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
2 changes: 1 addition & 1 deletion docs/state-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Component state is specific to individual components and should not be shared gl
- [useState](https://react.dev/reference/react/useState) - for simpler states that are independent
- [useReducer](https://react.dev/reference/react/useReducer) - for more complex states where on a single action you want to update several pieces of state

[Component State Example Code](../src/features/auth/components/register-form.tsx)
[Component State Example Code](../src/components/layouts/dashboard-layout.tsx)

## Application State

Expand Down
12 changes: 0 additions & 12 deletions src/app.tsx

This file was deleted.

24 changes: 24 additions & 0 deletions src/app/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useQueryClient } from '@tanstack/react-query';
import { useMemo } from 'react';
import { RouterProvider } from 'react-router-dom';

import { AppProvider } from './main-provider';
import { createRouter } from './routes';

const AppRouter = () => {
const queryClient = useQueryClient();

const router = useMemo(() => createRouter(queryClient), [queryClient]);

return <RouterProvider router={router} />;
};

function App() {
return (
<AppProvider>
<AppRouter />
</AppProvider>
);
}

export default App;
36 changes: 6 additions & 30 deletions src/providers/app.tsx → src/app/main-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,20 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import * as React from 'react';
import { useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { HelmetProvider } from 'react-helmet-async';
import { BrowserRouter as Router } from 'react-router-dom';

import { Button } from '@/components/ui/button';
import { MainErrorFallback } from '@/components/errors/main';
import { Notifications } from '@/components/ui/notifications';
import { Spinner } from '@/components/ui/spinner';
import { AuthLoader } from '@/features/auth';
import { queryConfig } from '@/lib/react-query';

const ErrorFallback = () => {
return (
<div
className="flex h-screen w-screen flex-col items-center justify-center text-red-500"
role="alert"
>
<h2 className="text-lg font-semibold">Ooops, something went wrong :( </h2>
<Button
className="mt-4"
onClick={() => window.location.assign(window.location.origin)}
>
Refresh
</Button>
</div>
);
};
import { AuthLoader } from '@/lib/auth';
import { queryClient } from '@/lib/react-query';

type AppProviderProps = {
children: React.ReactNode;
};

export const AppProvider = ({ children }: AppProviderProps) => {
const [queryClient] = useState(() => {
return new QueryClient({
defaultOptions: queryConfig,
});
});
return (
<React.Suspense
fallback={
Expand All @@ -47,7 +23,7 @@ export const AppProvider = ({ children }: AppProviderProps) => {
</div>
}
>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<ErrorBoundary FallbackComponent={MainErrorFallback}>
<HelmetProvider>
<QueryClientProvider client={queryClient}>
{import.meta.env.DEV && <ReactQueryDevtools />}
Expand All @@ -59,7 +35,7 @@ export const AppProvider = ({ children }: AppProviderProps) => {
</div>
)}
>
<Router>{children}</Router>
{children}
</AuthLoader>
</QueryClientProvider>
</HelmetProvider>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ContentLayout } from '@/components/layouts';
import { useUser, ROLES } from '@/features/auth';
import { useUser } from '@/lib/auth';
import { ROLES } from '@/lib/authorization';

export const DashboardRoute = () => {
const user = useUser();
Expand Down
Loading

0 comments on commit 05992c4

Please sign in to comment.