initial commit
This commit is contained in:
181
docs/react/context-with-custom-hook.md
Normal file
181
docs/react/context-with-custom-hook.md
Normal 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
4
docs/react/index.md
Normal 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)
|
308
docs/react/reading-env-vars-docker.md
Normal file
308
docs/react/reading-env-vars-docker.md
Normal 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.
|
Reference in New Issue
Block a user