initial commit
Some checks failed
Build, Test & Publish / Build and Publish Container Image (push) Has been cancelled
Build, Test & Publish / Deploy to Infrastructure (push) Has been cancelled
Build, Test & Publish / Build (push) Has been cancelled

This commit is contained in:
2024-09-05 13:54:08 +10:00
commit 8ad5845efc
57 changed files with 6046 additions and 0 deletions

View File

@ -0,0 +1,181 @@
# Combing Context and Custom Hooks
Combining context and custom hooks is a powerful way to manage state in your application. This pattern allows you to create a custom hook that can be used to access the context and state in a more readable and reusable way.
## React Setup
First we are going to assume that you have a react app created already. This could be done using a Vite SPA template, or by using a framework like Next.js or Gatsby.
## Create the Hook
We will get started by creating a new custom hook (usually I create a `hooks` directory and place them all there) to contain our logic. This hook will be used to access the context and state. Both JS and TS examples are provided below.
::: code-group
```jsx:line-numbers [useCounter.js]
import { createContext, useContext, useState } from 'react';
const CounterContext = createContext();
export const CounterProvider = ({ children }) => {
const [count, setCount] = useState(0);
const increaseCount = () => setCount(count + 1);
const decreaseCount = () => setCount(count - 1);
const context = {
count,
increaseCount,
decreaseCount
};
return (
<CounterContext.Provider value={context}>
{children}
</CounterContext.Provider>
);
}
export const useCounter = () => {
const context = useContext(CounterContext);
if (context === undefined) {
throw new Error('useCounter must be used within a CounterProvider');
}
return context;
};
```
```tsx:line-numbers [useCounter.tsx]
import { createContext, useContext, useState } from 'react';
type CounterContextType = {
count: number;
increaseCount: () => void;
decreaseCount: () => void;
};
const CounterContext = createContext<CounterContextType>({} as CounterContextType);
type CounterProviderProps = {
children: React.ReactNode;
};
export const CounterProvider = ({ children }: CounterProviderProps) => {
const [count, setCount] = useState(0);
const increaseCount = () => setCount(count + 1);
const decreaseCount = () => setCount(count - 1);
const context = {
count,
increaseCount,
decreaseCount
};
return (
<CounterContext.Provider value={context}>
{children}
</CounterContext.Provider>
);
}
export const useCounter = () => {
const context = useContext(CounterContext);
if (context === undefined) {
throw new Error('useCounter must be used within a CounterProvider');
}
return context;
};
```
:::
::: tip
If using TypeScript ensure that your hook file type is `.tsx` and that you have the correct types defined for your context and state.
:::
Lets go through this and explain it. First we create our Context using `const CounterContext = createContext();`. This will allow us to store our state and methods in a single place, to access anywhere in our app (assuming we have a `CounterProvider` wrapping our app).
Next we create our `CounterProvider` component. This will be used to wrap our app and provide the context to our custom hook. We use `useState` to create a `count` state and `setCount` method. We also create `increaseCount` and `decreaseCount` methods to update the `count` state. We will then create our context value, which is simply the output data we need (`count`), and the methods we can use to interact with and update it (`increaseCount` and `decreaseCount`).
Finally we create our `useCounter` custom hook. This hook will be used to access the context and state in a more readable and reusable way. We use `useContext` to access the context, and then check if the context is `undefined`, which will only be the case if the hook is being used outside of the provider.
## Using the Hook
Now that we have our custom hook, we can use it in our app to access the same context and state in different components. See below for some sample usage, in this example the buttons and couter have been separated into their own components.
::: code-group
```jsx:line-numbers [components/Buttons.js]
import { useCounter } from "../hooks/useCounter";
const Buttons = () => {
const { increaseCount, decreaseCount } = useCounter();
return (
<div>
<button onClick={increaseCount}>Increase Count +</button>
<button onClick={decreaseCount} >Decrease Count -</button>
</div>
);
};
export default Buttons;
```
```jsx:line-numbers [components/Count.js]
import { useCounter } from "../hooks/useCounter";
const Count = () => {
const { count } = useCounter();
return (
<div>
<h1>Count: {count}</h1>
</div>
);
}
export default Count;
```
```jsx:line-numbers [App.js]
import './App.css';
import { CounterProvider } from './hooks/useCounter';
import Buttons from './components/Buttons';
import Count from './components/Count';
function App() {
return (
<>
<CounterProvider>
<div className="App">
<header className="App-header">
<Buttons />
<Count />
</header>
</div>
</CounterProvider>
</>
);
}
export default App;
```
:::
When the above app is run and the buttons are clicked, the count will increase and decrease, and the `Count` component will update to reflect the new count.
## Conclusion
Combining context and custom hooks is a really powerful way to manage state in your application. Often using TypeScript can help to make this pattern even more powerful, as you can define the shape of the context and state, and ensure that the correct data is being passed around your app.
You can also do all your data fetching and API calls in the custom hook, and then pass the data to the context, which can then be accessed by any component in your app. This is a great way to manage your state and data in a more readable and reusable way.

4
docs/react/index.md Normal file
View File

@ -0,0 +1,4 @@
# React Snippets and Musings
#### [Context with Custom Hook](./context-with-custom-hook.md)
#### [Reading Config Values from a Docker Container in React](./reading-env-vars-docker.md)

View File

@ -0,0 +1,308 @@
# Reading Config Vars in Docker
Often you will need to read in config values in a react app, for example when connecting to a backend API, or when using config values to change various UI elements or control enabled features.
When using a full stack framework such as Next.JS or Gatsby, you can use the `process.env` object to read in environment variables as these applications are both server and client side rendered.
If we are using a client side only framework we will not have the luxury of using `process.env`. In this case we need to be able to load in our own configuration values into the app running in a Docker container. To this end we can create a `config.json` file, and serve this file in the Docker container mounted as a volume.
For this demo we'll be using a simple Vite react frontend, with an ExpressJS backend. We'll be using Docker to containerize our application.
## Creating a Backend
### Setup
First get started by creating a `backend` directory, and inside it a `package.json` with the following content:
::: code-group
```json [backend/package.json]
{
"name": "backend"
}
```
:::
### Installing Dependencies
```bash
cd backend
npm install typescript express cors
npm install -D @types/express @types/cors @types/node ts-node
```
### Creating the backend
In our case the backend will be a simple ExpressJS server that returns a list of users, so we can make this all one file:
::: code-group
```typescript [backend/src/index.ts]
import express from "express";
import cors from "cors";
const USERS = [
{ id: 1, name: 'John Doe', email: 'john.doe@gmail.com' },
{ id: 2, name: 'Jane Doe', email: 'jane.doe@gmail.com' },
{ id: 3, name: 'John Smith', email: 'john.smith@gmail.com' }
]
const app = express();
app.use(cors());
app.get('/', (_, res) => {
res.send('Hello World!');
});
app.get('/users', (_, res) => {
res.json(USERS);
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
```
:::
### Setting Up Typescript
Create a `tsconfig.json` file in the `backend` directory with the following content:
::: code-group
```json [backend/tsconfig.json]
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"rootDir": "./src",
"outDir": "./dist",
}
}
```
:::
### Add Scripts
Add the following scripts to the `package.json` file:
::: code-group
```json [backend/package.json]
{
"scripts": {
"dev": "ts-node src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
}
```
:::
## Creating a Frontend
### Setup
Create a new Vite React TS app using the command `npm create vite@latest frontend -- --template react-ts`, ensuring you are in the parent directory of the `backend` directory.
### Ignore the Local Config File
Update the `.gitignore` file to ignore the `config.json` file we will be creating later:
::: code-group
``` [frontend/.gitignore]
# ...
public/config.json
```
:::
### Create Config File
Create a `config.js` file in the `public` directory with the following content:
::: code-group
```js [frontend/public/config.js]
const config = {
apiUrl: 'http://localhost:3000'
}
window.config = config;
```
:::
Next update the `index.html` file to include the `config.json` file:
::: code-group
```html [frontend/public/index.html]
<!-- ... -->
<script src="/config.js"></script>
</head>
<!-- ... -->
```
:::
### Create a Config Util
Create a `config.ts` file in the `src` directory with the following content:
::: code-group
```typescript [frontend/src/config.ts]
export interface Config {
apiUrl: string;
}
export const config = (window as any).config as Config;
```
:::
### Update App
Update the app to call the backend API:
::: code-group
```tsx [frontend/src/App.tsx]
import { useEffect, useState } from "react"
import { config } from "./config"
type User = {
id: number,
name: string
email: string
}
function App() {
const [users, setUsers] = useState([] as User[])
useEffect(() => {
const fetchUsers = async () => {
const response = await fetch(`${config.apiUrl}/users`)
const data = await response.json()
setUsers(data)
}
fetchUsers();
}, [])
return (
<>
<h1>Users</h1>
<br />
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} - ({user.email})
</li>
))}
</ul>
</>
)
}
export default App
```
:::
# Running the Application
At this stage you will be able to run the application locally by running the backend and frontend separately. The backend will expose the user list, and the frontend will display the list of users.
# Dockerise
Finally the last step is dockerising the application. Create the following Dockerfiles, docker-compose and config files in the root of the project:
::: code-group
``` [Dockerfile.backend]
FROM node:alpine
WORKDIR /app
COPY package.json .
COPY package-lock.json .
RUN npm install
COPY . .
RUN npm run build
CMD ["npm", "start"]
```
``` [Dockerfile.frontend]
FROM node:alpine as build
WORKDIR /app
COPY package.json .
COPY package-lock.json .
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
ENTRYPOINT ["nginx","-g","daemon off;"]
```
```yml [compose.yml]
services:
api:
build:
context: './backend'
dockerfile: '../Dockerfile.backend'
ports:
- "8081:3000"
web:
build:
context: './frontend'
dockerfile: '../Dockerfile.frontend'
ports:
- "8080:80"
volumes:
- './config-production.js:/usr/share/nginx/html/config.js'
```
```js [config-production.js]
const config = {
apiUrl: 'http://localhost:8081'
}
window.config = config;
```
:::
If you then run `docker-compose up` you should see the application running in a Docker container and when navigating to `http://localhost:8080`.
# Conclusion
In this guide we have seen how to read config values in a React app running in a Docker container. We have created a `config.js` file that we use to store our config values, and then use a volume to mount this file into the Docker container. This allows us to read the config values in our React app.
As a follow up this `config.js` file would be created per environment, or even created as part of the CI/CD pipeline, so that the correct values are used for each environment. Please note however as this is a client side app all values will be visible to the end user, so do not store any sensitive information in the `config.js` file.