├── .gitignore
├── vite.config.js
├── src
├── main.jsx
├── index.css
├── App.jsx
├── App.css
├── favicon.svg
└── Pokemon.jsx
├── index.html
├── package.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import reactRefresh from '@vitejs/plugin-react-refresh'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [reactRefresh()]
7 | })
8 |
--------------------------------------------------------------------------------
/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 |
4 | import "./index.css";
5 | import App from "./App";
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById("root")
12 | );
13 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite App
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pokemon-feature-flags",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "dev": "vite",
6 | "build": "vite build",
7 | "serve": "vite preview"
8 | },
9 | "dependencies": {
10 | "launchdarkly-react-client-sdk": "^2.22.2",
11 | "react": "^17.0.0",
12 | "react-dom": "^17.0.0"
13 | },
14 | "devDependencies": {
15 | "@vitejs/plugin-react-refresh": "^1.3.1",
16 | "vite": "^2.3.3"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Pokemon from "./Pokemon";
3 | import { withLDProvider } from "launchdarkly-react-client-sdk";
4 |
5 | function App() {
6 | return (
7 | <>
8 |
9 | >
10 | );
11 | }
12 |
13 | export default withLDProvider({
14 | clientSideID: import.meta.env
15 | .VITE_LD_CLIENT_KEY,
16 | user: {
17 | key: "user_key",
18 | name: "User name",
19 | email: "User@email.com",
20 | },
21 | })(App);
22 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: "Arial", sans-serif;
4 | -webkit-font-smoothing: antialiased;
5 | -moz-osx-font-smoothing: grayscale;
6 | font-size: 20px;
7 | }
8 |
9 | .pokemon {
10 | position: relative;
11 | margin: 20px auto;
12 | width: 400px;
13 | height: 400px;
14 | border: 10px solid #282828;
15 | border-radius: 50%;
16 | overflow: hidden;
17 | text-align: center;
18 | }
19 |
20 | .pokemon div {
21 | display: flex;
22 | align-items: center;
23 | margin-bottom: 40px;
24 | background: #d5615e;
25 | height: 50%;
26 | }
27 | .pokemon input {
28 | margin: auto;
29 | padding: 5px;
30 | display: block;
31 | border: 3px solid #282828;
32 | border-radius: 10px;
33 | font-size: 20px;
34 | &:focus {
35 | outline: 3px dashed #282828;
36 | }
37 | }
38 |
39 | .pokemon span {
40 | text-transform: capitalize;
41 | }
42 |
43 | .pokemon img {
44 | margin: auto;
45 | display: block;
46 | }
47 |
--------------------------------------------------------------------------------
/src/favicon.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pokémon Feature Flags demo
2 |
3 | [](https://app.netlify.com/sites/pokemon-ld/deploys)
4 |
5 | Here's a demo for integrating feature flags into a React project! Built with React, Vite, [the PokeAPI](https://pokeapi.co/), and [LaunchDarkly](https://launchdarkly.com/)!
6 |
7 | When the feature flag is off, only Normal type Pokémon can be searched for in the app. When the flag is turned on, then any Pokémon can be queried!
8 |
9 | ## Deploy your own
10 |
11 | ### LaunchDarkly setup
12 |
13 | Once you've signed up for [LaunchDarkly](https://launchdarkly.com/), create a feature flag. I call mine `testaroni`, and you should too if you want this to work out of the box. Otherwise, you can change it in the `isAllowed` function in `Pokemon.jsx`. Under **Flag variations**, make it a **String** type and add an "all" type, and any other Pokémon types you'd like to include:
14 |
15 | 
16 |
17 | After that, you're all set! You can toggle it on and off in the LaunchDarkly UI, set the default rules, segment which users get which types, and more.
18 |
19 | ### Netlify setup
20 |
21 | Click this button to deploy your own version of this project to Netlify!
22 |
23 | [](https://app.netlify.com/start/deploy?repository=https://github.com/cassidoo/pokemon-feature-flags&utm_source=github&utm_medium=ldstream-cs&utm_campaign=devex-cs)
24 |
25 | You'll need to add your environment variables after cloning. Structure your `.env.local` file like so (you can find your client ID under [Account Settings => Projects](https://app.launchdarkly.com/settings/projects)):
26 |
27 | ```
28 | VITE_LD_CLIENT_KEY=your_key
29 | ```
30 |
31 | ...and then plop it into Netlify, and you're done!
32 |
--------------------------------------------------------------------------------
/src/Pokemon.jsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | useState,
3 | useEffect,
4 | } from "react";
5 | import { withLDConsumer } from "launchdarkly-react-client-sdk";
6 |
7 | import "./App.css";
8 |
9 | const Pokemon = ({ flags }) => {
10 | const [pokémon, setPokémon] =
11 | useState("pidgey");
12 | const [img, setImg] = useState("pidgey");
13 |
14 | useEffect(() => {
15 | document.title = "Hello, " + pokémon;
16 | }, [pokémon]);
17 |
18 | useEffect(() => {
19 | let isCurrent = true;
20 |
21 | if (pokémon.length >= 4) {
22 | fetch(
23 | `https://pokeapi.co/api/v2/pokemon/${pokémon}/`
24 | )
25 | .then((res) => res.json())
26 | .then((res) => {
27 | if (
28 | isCurrent &&
29 | isAllowed(res.types, flags)
30 | ) {
31 | setPokémon(
32 | res.name.replace(/^\w/, (c) =>
33 | c.toUpperCase()
34 | )
35 | ); // capitalizing name
36 | setImg(res.sprites.front_default);
37 | }
38 | })
39 | .catch((error) => {
40 | console.log(error);
41 | });
42 | }
43 |
44 | return () => {
45 | isCurrent = false;
46 | };
47 | }, [pokémon]);
48 |
49 | return (
50 | <>
51 |
52 |
53 |
55 | setPokémon(e.target.value)
56 | }
57 | defaultValue={pokémon}
58 | type="text"
59 | />
60 |
61 | Hello, {pokémon}!
62 |

63 |
64 | >
65 | );
66 | };
67 |
68 | function isAllowed(types, flags) {
69 | return (
70 | flags.testaroni === "all" ||
71 | types
72 | .map((t) => {
73 | return t.type.name;
74 | })
75 | .some((e) => {
76 | return e === flags.testaroni;
77 | })
78 | );
79 | }
80 |
81 | export default withLDConsumer()(Pokemon);
82 |
--------------------------------------------------------------------------------