├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── bug_report.md │ ├── config.yml │ ├── custom.md │ ├── feature_request.md │ └── feature_request.yml ├── pull_request_template.md └── workflows │ └── greet.yml ├── .gitignore ├── .prettierrc.json ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app ├── example.ts ├── examples │ ├── examples.css │ ├── hackerNews │ │ ├── code.ts │ │ └── page.tsx │ ├── page.tsx │ ├── textLenght │ │ ├── code.ts │ │ └── page.tsx │ └── todolist │ │ ├── code.ts │ │ ├── page.tsx │ │ └── styles.ts ├── globals.css ├── head.tsx ├── layout.tsx ├── page.tsx ├── playground │ ├── code.ts │ ├── page.tsx │ └── playground.css ├── quick-start │ ├── Hamburger.tsx │ ├── Header.tsx │ ├── async-read-atoms │ │ ├── code.ts │ │ ├── markdown.ts │ │ ├── page.tsx │ │ └── styles.ts │ ├── async-write-atoms │ │ ├── code.ts │ │ ├── markdown.ts │ │ ├── page.tsx │ │ └── styles.ts │ ├── atom-creators │ │ ├── code.ts │ │ ├── markdown.ts │ │ ├── page.tsx │ │ └── styles.ts │ ├── hamburger.css │ ├── immer-integration │ │ ├── code.ts │ │ ├── markdown.ts │ │ ├── page.tsx │ │ └── styles.ts │ ├── intro │ │ ├── code.ts │ │ ├── markdown.ts │ │ ├── page.tsx │ │ └── styles.ts │ ├── layout.tsx │ ├── official-utils │ │ ├── code.ts │ │ ├── markdown.ts │ │ ├── page.tsx │ │ └── styles.ts │ ├── persisting-state │ │ ├── code.ts │ │ ├── markdown.ts │ │ ├── page.tsx │ │ └── styles.ts │ ├── read-write-atoms │ │ ├── code.ts │ │ ├── markdown.ts │ │ ├── page.tsx │ │ └── styles.ts │ ├── readonly-atoms │ │ ├── code.ts │ │ ├── markdown.ts │ │ ├── page.tsx │ │ └── styles.ts │ ├── theme-setting │ │ ├── code.ts │ │ ├── markdown.ts │ │ ├── page.tsx │ │ └── styles.ts │ └── write-only-atoms │ │ ├── code.ts │ │ ├── markdown.ts │ │ ├── page.tsx │ │ ├── styles.ts │ │ └── write-only-atom.png └── vscDarkPlus.js ├── declaration.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── public ├── github.png └── jotai-mascot.png └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug 2 | description: Report an issue to help improve the project. 3 | labels: ["🛠 goal: fix"] 4 | body: 5 | - type: textarea 6 | id: description 7 | attributes: 8 | label: Description 9 | description: A brief description of the question or issue, also include what you tried and what didn't work 10 | validations: 11 | required: true 12 | - type: textarea 13 | id: screenshots 14 | attributes: 15 | label: Screenshots 16 | description: Please add screenshots if applicable 17 | validations: 18 | required: false 19 | - type: textarea 20 | id: extrainfo 21 | attributes: 22 | label: Additional information 23 | description: Is there anything else we should know about this bug? 24 | validations: 25 | required: false 26 | - type: markdown 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 💡 General Feature Request 2 | description: Have a new idea/feature for tutorials? Please suggest! 3 | title: "[FEATURE] " 4 | labels: ["⭐ goal: addition"] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: A brief description of the enhancement you propose, also include what you tried and what worked. 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: screenshots 15 | attributes: 16 | label: Screenshots 17 | description: Please add screenshots if applicable 18 | validations: 19 | required: false 20 | - type: textarea 21 | id: extrainfo 22 | attributes: 23 | label: Additional information 24 | description: Is there anything else we should know about this idea? 25 | validations: 26 | required: false 27 | - type: markdown 28 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Fixes Issue 4 | 5 | 6 | 7 | 8 | 9 | ## Changes proposed 10 | 11 | 12 | 13 | 14 | 20 | 21 | ## Check List (Check all the applicable boxes) 22 | 23 | - [ ] My code follows the code style of this project. 24 | - [ ] My change requires changes to the documentation. 25 | - [ ] All new and existing tests passed. 26 | - [ ] This PR does not contain plagiarized content. 27 | - [ ] The title of my pull request is a short description of the requested changes. 28 | 29 | ## Screenshots 30 | 31 | 32 | 33 | ## Note to reviewers 34 | 35 | 36 | -------------------------------------------------------------------------------- /.github/workflows/greet.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request_target, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | steps: 12 | - uses: actions/first-interaction@v1 13 | with: 14 | repo-token: ${{ secrets.GITHUB_TOKEN }} 15 | issue-message: "Awesome! Thanks for taking the time to open an issue. We will have a look and answer as soon as we can.' first issue" 16 | pr-message: "Thank you for opening an PR. We will review the PR and provide feedback as soon as we can.' first pr" 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | [discord](https://discord.com/channels/627656437971288081/819691374835662889). 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Reporting Issues 4 | 5 | If you have found what you think is a bug, please file an issue. 6 | 7 | For usage questions, prefer [this thread](https://github.com/pmndrs/jotai/discussions/1638). 8 | 9 | ## Suggesting new features 10 | 11 | If you are here to suggest a feature, first create an issue if it does not already exist. From there, we will discuss use-cases for the feature and then finally discuss how it could be implemented. 12 | 13 | ## Development 14 | 15 | If you would like to contribute by fixing an open issue or developing a new feature you can use this suggested workflow: 16 | 17 | - Fork this repository. 18 | - Create a new feature branch based off the `main` branch. 19 | - Install dependencies by running `$ npm install`. 20 | - Run `$ npm run dev` and start working on the feature. 21 | 22 | ## Pull requests 23 | 24 | Please try to keep your pull request focused in scope and avoid including unrelated commits. 25 | 26 | After you have submitted your pull request, we'll try to get back to you as soon as possible. We may suggest some changes or improvements. 27 | 28 | Thank you for contributing! 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jotai Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Learn **Jotai** without leaving your browser. 2 | 3 | ![Jotai❤️](https://user-images.githubusercontent.com/85363195/214372347-f7142c0e-02e2-4cf7-b8c4-5b13096241cb.gif) 4 | 5 | - ✅ Interactive Lessons. 6 | - ✅ Showcase Examples. 7 | - ✅ Live Playground. 8 | 9 | ## Lessons 10 | - [Introduction](https://jotai-tutorial.netlify.app/quick-start/intro) 11 | - [Theme Setup](https://jotai-tutorial.netlify.app/quick-start/theme-setting) 12 | - [Persisting State](https://jotai-tutorial.netlify.app/quick-start/persisting-state) 13 | - [Read Only Atoms](https://jotai-tutorial.netlify.app/quick-start/readonly-atoms) 14 | - [Write Only Atoms](https://jotai-tutorial.netlify.app/quick-start/write-only-atoms) 15 | - [Read Write Atoms](https://jotai-tutorial.netlify.app/quick-start/read-write-atoms) 16 | - [Atom Creators](https://jotai-tutorial.netlify.app/quick-start/atom-creators) 17 | - [Async Read Atoms](https://jotai-tutorial.netlify.app/quick-start/async-read-atoms) 18 | - [Async Write Atoms](https://jotai-tutorial.netlify.app/quick-start/async-write-atoms) 19 | - [Official Utils](https://jotai-tutorial.netlify.app/quick-start/official-utils) 20 | - [Integrations](https://jotai-tutorial.netlify.app/quick-start/immer-integration) 21 | 22 | For detail documentation headover to [jotai.org](https://jotai.org/). 23 | -------------------------------------------------------------------------------- /app/example.ts: -------------------------------------------------------------------------------- 1 | export const example = ` 2 | ~~~js 3 | import { atom, useAtom } from 'jotai'; 4 | 5 | const counter = atom(0); 6 | 7 | export default function Page() { 8 | const [count, setCounter] = useAtom(counter); 9 | const onClick = () => { 10 | setCounter(prev => prev + 1); 11 | } 12 | return ( 13 |
14 |

{count}

15 | 16 |
17 | ) 18 | } 19 | ~~~ 20 | `; 21 | -------------------------------------------------------------------------------- /app/examples/examples.css: -------------------------------------------------------------------------------- 1 | .ex-switcher { 2 | position: absolute; 3 | top: 2%; 4 | right: 1%; 5 | } 6 | 7 | .dark { 8 | background: black; 9 | color: white; 10 | } 11 | 12 | .dark-head { 13 | color: white !important; 14 | border-bottom: 2px solid white !important; 15 | } 16 | 17 | .dark-des { 18 | color: #737374; 19 | } 20 | 21 | .dark-ex-btn { 22 | color: white !important; 23 | } 24 | 25 | .dark-ex-btn:hover { 26 | background: #0d3532 !important; 27 | } 28 | 29 | .example-btn { 30 | font-size: 15px; 31 | padding: 5px; 32 | background: transparent; 33 | font-weight: 300; 34 | height: 35px; 35 | letter-spacing: 0.2px; 36 | border: none; 37 | outline: none; 38 | cursor: pointer; 39 | border-radius: 10px; 40 | margin: 8px; 41 | } 42 | 43 | .example-btn:hover { 44 | background: #dbe9fe; 45 | } 46 | 47 | .examples { 48 | min-height: 100vh; 49 | display: flex; 50 | flex-direction: column; 51 | align-items: center; 52 | } 53 | 54 | .cont-head { 55 | margin-top: 80px; 56 | } 57 | 58 | .exp-cont { 59 | max-width: 600px; 60 | display: flex; 61 | flex-direction: column; 62 | justify-content: center; 63 | align-items: center; 64 | margin-top: 30px; 65 | } 66 | 67 | .li-head { 68 | color: black; 69 | font-size: 20px; 70 | font-weight: 400; 71 | border-bottom: 2px solid black; 72 | } 73 | 74 | .li-des { 75 | margin-top: 5px; 76 | margin-bottom: 25px; 77 | } 78 | 79 | @media screen and (max-width: 700px) { 80 | .exp-cont { 81 | max-width: 400px; 82 | } 83 | } -------------------------------------------------------------------------------- /app/examples/hackerNews/code.ts: -------------------------------------------------------------------------------- 1 | export const code = `import { Suspense } from 'react' 2 | import { a, useSpring } from '@react-spring/web' 3 | import Parser from 'html-react-parser' 4 | import { Provider, atom, useAtom, useSetAtom } from 'jotai' 5 | 6 | type PostData = { 7 | by: string 8 | descendants?: number 9 | id: number 10 | kids?: number[] 11 | parent: number 12 | score?: number 13 | text?: string 14 | time: number 15 | title?: string 16 | type: 'comment' | 'story' 17 | url?: string 18 | } 19 | 20 | const postId = atom(9001) 21 | const postData = atom(async (get) => { 22 | const id = get(postId) 23 | const response = await fetch( 24 | \`https://hacker-news.firebaseio.com/v0/item/\${id}.json\` 25 | ) 26 | const data: PostData = await response.json() 27 | return data 28 | }) 29 | 30 | function Id() { 31 | const [id] = useAtom(postId) 32 | const props = useSpring({ from: { id }, id, reset: true }) 33 | return {props.id.to(Math.round)} 34 | } 35 | 36 | function Next() { 37 | // const [, set] = useAtom(postId) 38 | const setPostId = useSetAtom(postId) 39 | return ( 40 | 43 | ) 44 | } 45 | 46 | function PostTitle() { 47 | const [{ by, text, time, title, url }] = useAtom(postData) 48 | return ( 49 | <> 50 |

{by}

51 |
{new Date(time * 1000).toLocaleDateString('en-US')}
52 | {title &&

{title}

} 53 | {url && {url}} 54 | {text &&
{Parser(text)}
} 55 | 56 | ) 57 | } 58 | 59 | export default function App() { 60 | return ( 61 | 62 | 63 |
64 | Loading...}> 65 | 66 | 67 |
68 | 69 |
70 | ) 71 | } 72 | ` 73 | 74 | export const code1 = `@import url("https://rsms.me/inter/inter.css"); 75 | 76 | * { 77 | box-sizing: border-box; 78 | outline: none !important; 79 | } 80 | 81 | html, 82 | body, 83 | #root { 84 | width: 100%; 85 | height: 100%; 86 | margin: 0; 87 | padding: 0; 88 | } 89 | 90 | body { 91 | background: white; 92 | color: black; 93 | font-family: "Inter", sans-serif; 94 | -webkit-font-smoothing: antialiased; 95 | -moz-osx-font-smoothing: grayscale; 96 | } 97 | 98 | #root { 99 | display: grid; 100 | grid-template-columns: auto 1fr auto; 101 | } 102 | 103 | h1 { 104 | writing-mode: tb-rl; 105 | font-variant-numeric: tabular-nums; 106 | font-weight: 700; 107 | font-size: 10em; 108 | letter-spacing: -10px; 109 | text-align: left; 110 | margin: 0; 111 | padding: 50px 0px 0px 20px; 112 | } 113 | 114 | h2 { 115 | margin-bottom: 0.2em; 116 | } 117 | 118 | h4 { 119 | font-weight: 500; 120 | } 121 | 122 | h6 { 123 | margin-top: 0; 124 | } 125 | 126 | #root > div { 127 | padding: 50px 20px; 128 | overflow: hidden; 129 | word-wrap: break-word; 130 | position: relative; 131 | } 132 | 133 | #root > div > div { 134 | position: absolute; 135 | } 136 | 137 | p { 138 | color: #474747; 139 | } 140 | 141 | button { 142 | text-decoration: none; 143 | background: transparent; 144 | border: none; 145 | cursor: pointer; 146 | font-family: "Inter", sans-serif; 147 | font-weight: 200; 148 | font-size: 6em; 149 | padding: 0px 30px 20px 0px; 150 | display: flex; 151 | align-items: flex-end; 152 | color: inherit; 153 | } 154 | 155 | button:focus { 156 | outline: 0; 157 | } 158 | 159 | a { 160 | color: inherit; 161 | } 162 | ` -------------------------------------------------------------------------------- /app/examples/hackerNews/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import "../../playground/playground.css" 4 | import { 5 | SandpackProvider, 6 | SandpackLayout, 7 | SandpackCodeEditor, 8 | SandpackPreview, 9 | SandpackFileExplorer, 10 | useSandpack, 11 | } from "@codesandbox/sandpack-react"; 12 | import { useEffect, useState } from "react"; 13 | import { code, code1 } from "./code"; 14 | import { AiFillFileAdd, AiOutlineDelete, AiOutlineHome } from "react-icons/ai" 15 | import { BsSun, BsFillMoonStarsFill } from "react-icons/bs" 16 | 17 | import { themeAtom } from "../../quick-start/Header"; 18 | import { useAtom } from "jotai"; 19 | 20 | const NewFile = () => { 21 | const [theme, setTheme] = useAtom(themeAtom); 22 | const { sandpack } = useSandpack(); 23 | const handleClick = () => { 24 | setTheme(); 25 | } 26 | 27 | function updatedFiles() { 28 | useEffect(() => { 29 | sandpack.updateFile({ '/App.tsx': code, '/styles.css': code1}); 30 | }, []) 31 | } 32 | 33 | updatedFiles(); 34 | 35 | const fileAdd = () => { 36 | const newName = (window.prompt('Enter the new file name:')); 37 | sandpack.addFile(newName ? newName : 'Untitled') 38 | } 39 | 40 | const fileDelete = () => { 41 | const filepath = sandpack.activeFile 42 | console.log(filepath) 43 | sandpack.deleteFile(filepath) 44 | } 45 | 46 | return ( 47 | <> 48 |
49 | 50 | 53 | 54 | 57 | 60 |
61 | 62 |
63 | 64 | 67 | 68 | 69 | 72 | 73 |
74 | 75 |
76 | {theme === 'light' ? : } 77 |
78 | 79 | ) 80 | } 81 | 82 | function Page() { 83 | const [theme] = useAtom(themeAtom) 84 | const [file, setFile] = useState({}) 85 | return ( 86 |
87 | 100 |
101 | 102 |
103 | 104 | 108 | 109 | 110 | 111 | 112 |
113 |
114 | ) 115 | } 116 | 117 | export default Page -------------------------------------------------------------------------------- /app/examples/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React from 'react' 4 | import './examples.css' 5 | import { BsSun, BsFillMoonStarsFill } from "react-icons/bs" 6 | 7 | import { themeAtom } from "../quick-start/Header"; 8 | import { useAtom } from 'jotai' 9 | 10 | function Page() { 11 | const [theme, setTheme] = useAtom(themeAtom); 12 | const handleClick = () => { 13 | setTheme(); 14 | } 15 | 16 | return ( 17 |
18 |
19 | 20 | 23 | 24 | 25 | 28 | 29 |
30 | 31 | {theme === 'light' ? : } 32 |

Examples

33 |
34 |
    35 |
  • 36 | Text Length example 37 |

    Count the length and show the uppercase of any text.

    38 |
  • 39 |
  • 40 | Todos example 41 |

    Record your todo list by typing them into this app, check them off if you have completed the task, and switch between Completed and Incompleted to see the status of your task.

    42 |
  • 43 |
  • 44 | Hacker News example 45 |

    Demonstrate a news article with Jotai, hit next to see more articles.

    46 |
  • 47 |
48 |
49 |
50 | ) 51 | } 52 | 53 | export default Page -------------------------------------------------------------------------------- /app/examples/textLenght/code.ts: -------------------------------------------------------------------------------- 1 | export const code = `import { Provider, atom, useAtom } from 'jotai' 2 | 3 | const textAtom = atom('hello') 4 | const textLenAtom = atom((get) => get(textAtom).length) 5 | const uppercaseAtom = atom((get) => get(textAtom).toUpperCase()) 6 | 7 | const Input = () => { 8 | const [text, setText] = useAtom(textAtom) 9 | return setText(e.target.value)} /> 10 | } 11 | 12 | const CharCount = () => { 13 | const [len] = useAtom(textLenAtom) 14 | return
Length: {len}
15 | } 16 | 17 | const Uppercase = () => { 18 | const [uppercase] = useAtom(uppercaseAtom) 19 | return
Uppercase: {uppercase}
20 | } 21 | 22 | const App = () => ( 23 | 24 | 25 | 26 | 27 | 28 | ) 29 | 30 | export default App 31 | ` -------------------------------------------------------------------------------- /app/examples/textLenght/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import "../../playground/playground.css" 4 | import { 5 | SandpackProvider, 6 | SandpackLayout, 7 | SandpackCodeEditor, 8 | SandpackPreview, 9 | SandpackFileExplorer, 10 | useSandpack, 11 | } from "@codesandbox/sandpack-react"; 12 | import { useEffect, useState } from "react"; 13 | import { code } from "./code"; 14 | import { AiFillFileAdd, AiOutlineDelete, AiOutlineHome } from "react-icons/ai" 15 | import { BsSun, BsFillMoonStarsFill } from "react-icons/bs" 16 | 17 | import { themeAtom } from "../../quick-start/Header"; 18 | import { useAtom } from "jotai"; 19 | 20 | const NewFile = () => { 21 | const [theme, setTheme] = useAtom(themeAtom); 22 | const { sandpack } = useSandpack(); 23 | const handleClick = () => { 24 | setTheme(); 25 | } 26 | 27 | function updatedFiles() { 28 | useEffect(() => { 29 | sandpack.updateFile('/App.tsx', code); 30 | }, []) 31 | } 32 | 33 | updatedFiles(); 34 | 35 | const fileAdd = () => { 36 | const newName = (window.prompt('Enter the new file name:')); 37 | sandpack.addFile(newName ? newName : 'Untitled') 38 | } 39 | 40 | const fileDelete = () => { 41 | const filepath = sandpack.activeFile 42 | console.log(filepath) 43 | sandpack.deleteFile(filepath) 44 | } 45 | 46 | return ( 47 | <> 48 |
49 | 50 | 53 | 54 | 57 | 60 |
61 | 62 |
63 | 64 | 67 | 68 | 69 | 72 | 73 |
74 | 75 |
76 | {theme === 'light' ? : } 77 |
78 | 79 | ) 80 | } 81 | 82 | function Page() { 83 | const [theme] = useAtom(themeAtom) 84 | const [file, setFile] = useState({}) 85 | return ( 86 |
87 | 99 |
100 | 101 |
102 | 103 | 107 | 108 | 109 | 110 | 111 |
112 |
113 | ) 114 | } 115 | 116 | export default Page -------------------------------------------------------------------------------- /app/examples/todolist/code.ts: -------------------------------------------------------------------------------- 1 | export const code = `import type { FormEvent } from 'react' 2 | import { CloseOutlined } from '@ant-design/icons' 3 | import { a, useTransition } from '@react-spring/web' 4 | import { Radio } from 'antd' 5 | import { Provider, atom, useAtom, useSetAtom } from 'jotai' 6 | import type { PrimitiveAtom } from 'jotai' 7 | 8 | type Todo = { 9 | title: string 10 | completed: boolean 11 | } 12 | 13 | const filterAtom = atom('all') 14 | const todosAtom = atom[]>([]) 15 | const filteredAtom = atom[]>((get) => { 16 | const filter = get(filterAtom) 17 | const todos = get(todosAtom) 18 | if (filter === 'all') return todos 19 | else if (filter === 'completed') 20 | return todos.filter((atom) => get(atom).completed) 21 | else return todos.filter((atom) => !get(atom).completed) 22 | }) 23 | 24 | type RemoveFn = (item: PrimitiveAtom) => void 25 | type TodoItemProps = { 26 | atom: PrimitiveAtom 27 | remove: RemoveFn 28 | } 29 | const TodoItem = ({ atom, remove }: TodoItemProps) => { 30 | const [item, setItem] = useAtom(atom) 31 | const toggleCompleted = () => 32 | setItem((props) => ({ ...props, completed: !props.completed })) 33 | return ( 34 | <> 35 | 40 | 41 | {item.title} 42 | 43 | remove(atom)} /> 44 | 45 | ) 46 | } 47 | 48 | const Filter = () => { 49 | const [filter, set] = useAtom(filterAtom) 50 | return ( 51 | set(e.target.value)} value={filter}> 52 | All 53 | Completed 54 | Incompleted 55 | 56 | ) 57 | } 58 | 59 | type FilteredType = { 60 | remove: RemoveFn 61 | } 62 | const Filtered = (props: FilteredType) => { 63 | const [todos] = useAtom(filteredAtom) 64 | const transitions = useTransition(todos, { 65 | keys: (todo) => todo.toString(), 66 | from: { opacity: 0, height: 0 }, 67 | enter: { opacity: 1, height: 40 }, 68 | leave: { opacity: 0, height: 0 }, 69 | }) 70 | return transitions((style, atom) => ( 71 | 72 | 73 | 74 | )) 75 | } 76 | 77 | const TodoList = () => { 78 | // const [, setTodos] = useAtom(todosAtom) 79 | const setTodos = useSetAtom(todosAtom) 80 | const remove: RemoveFn = (todo) => 81 | setTodos((prev) => prev.filter((item) => item !== todo)) 82 | const add = (e: FormEvent) => { 83 | e.preventDefault() 84 | const title = e.currentTarget.inputTitle.value 85 | e.currentTarget.inputTitle.value = '' 86 | setTodos((prev) => [...prev, atom({ title, completed: false })]) 87 | } 88 | return ( 89 |
90 | 91 | 92 | 93 | 94 | ) 95 | } 96 | 97 | export default function App() { 98 | return ( 99 | 100 |

Jōtai

101 | 102 |
103 | ) 104 | } 105 | ` 106 | 107 | export const code1 = `import { render } from 'react-dom' 108 | import 'antd/dist/antd.css' 109 | import './todoStyles.css' 110 | import Todo from './Todo' 111 | 112 | render(, document.getElementById('root')) 113 | ` -------------------------------------------------------------------------------- /app/examples/todolist/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import "../../playground/playground.css" 4 | import { 5 | SandpackProvider, 6 | SandpackLayout, 7 | SandpackCodeEditor, 8 | SandpackPreview, 9 | SandpackFileExplorer, 10 | useSandpack, 11 | } from "@codesandbox/sandpack-react"; 12 | import { useEffect, useState } from "react"; 13 | import { code, code1 } from "./code"; 14 | import setupStyles from "./styles" 15 | import { AiFillFileAdd, AiOutlineDelete, AiOutlineHome } from "react-icons/ai" 16 | import { BsSun, BsFillMoonStarsFill } from "react-icons/bs" 17 | 18 | import { themeAtom } from "../../quick-start/Header"; 19 | import { useAtom } from "jotai"; 20 | 21 | const NewFile = () => { 22 | const [theme, setTheme] = useAtom(themeAtom); 23 | const { sandpack } = useSandpack(); 24 | const handleClick = () => { 25 | setTheme(); 26 | } 27 | 28 | function updatedFiles() { 29 | useEffect(() => { 30 | sandpack.updateFile('/index.tsx', code1); 31 | sandpack.deleteFile('/App.tsx') 32 | }, []) 33 | } 34 | 35 | updatedFiles(); 36 | 37 | const fileAdd = () => { 38 | const newName = (window.prompt('Enter the new file name:')); 39 | sandpack.addFile(newName ? newName : 'Untitled') 40 | } 41 | 42 | const fileDelete = () => { 43 | const filepath = sandpack.activeFile 44 | console.log(filepath) 45 | sandpack.deleteFile(filepath) 46 | } 47 | 48 | return ( 49 | <> 50 |
51 | 52 | 55 | 56 | 59 | 62 |
63 | 64 | 76 | 77 |
78 | {theme === 'light' ? : } 79 |
80 | 81 | ) 82 | } 83 | 84 | function Page() { 85 | const [theme] = useAtom(themeAtom) 86 | const [file, setFile] = useState({}) 87 | return ( 88 |
89 | 101 |
102 | 103 |
104 | 105 | 109 | 110 | 111 | 112 | 113 |
114 |
115 | ) 116 | } 117 | 118 | export default Page -------------------------------------------------------------------------------- /app/examples/todolist/styles.ts: -------------------------------------------------------------------------------- 1 | const todoStyles = ` 2 | @import url("https://rsms.me/inter/inter.css"); 3 | 4 | * { 5 | box-sizing: border-box; 6 | } 7 | 8 | html, 9 | body { 10 | width: 100%; 11 | height: 100%; 12 | } 13 | 14 | body { 15 | margin-top: 5em; 16 | display: flex; 17 | align-items: flex-start; 18 | justify-content: center; 19 | background: #fdfdfd; 20 | font-family: "Inter", sans-serif !important; 21 | -webkit-font-smoothing: antialiased; 22 | -moz-osx-font-smoothing: grayscale; 23 | filter: saturate(0); 24 | } 25 | 26 | #root { 27 | width: 50ch; 28 | display: flex; 29 | flex-direction: column; 30 | gap: 1em; 31 | } 32 | 33 | input:not([type="checkbox"]) { 34 | width: 100%; 35 | border: none; 36 | box-shadow: 0px 15px 30px rgba(0, 0, 0, 0.05); 37 | padding: 10px 20px; 38 | margin-top: 2em; 39 | margin-bottom: 4em; 40 | background: white; 41 | } 42 | 43 | input:focus { 44 | outline: none; 45 | } 46 | 47 | .anticon-close { 48 | width: 32px !important; 49 | cursor: pointer; 50 | color: #c0c0c0; 51 | } 52 | 53 | .anticon-close:hover { 54 | color: #272730; 55 | } 56 | 57 | .item { 58 | position: relative; 59 | display: flex; 60 | width: 100%; 61 | align-items: center; 62 | justify-content: space-between; 63 | gap: 20px; 64 | overflow: hidden; 65 | } 66 | 67 | .item > span { 68 | display: inline-block; 69 | width: 100%; 70 | } 71 | 72 | h1 { 73 | font-size: 10em; 74 | font-weight: 800; 75 | margin: 0; 76 | padding: 0; 77 | letter-spacing: -5px; 78 | color: black; 79 | white-space: nowrap; 80 | } 81 | ` 82 | 83 | const setupStyles = { 84 | "/todoStyles.css": { 85 | code: todoStyles, 86 | }, 87 | }; 88 | 89 | export default setupStyles; -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | .light { 7 | --selection-background: black; 8 | --selection-text-color: #fafafa; 9 | } 10 | 11 | ::selection { 12 | background: var(--selection-background); 13 | color: var(--selection-text-color); 14 | } 15 | 16 | .sp-stack { 17 | width: 100% !important; 18 | } 19 | 20 | .sp-syntax-keyword, 21 | .sp-syntax-tag { 22 | color: #82b4e2 !important; 23 | } 24 | 25 | .sp-syntax-definition, 26 | .sp-syntax-string { 27 | color: #3ccaad !important; 28 | } 29 | 30 | .sp-c-dRhJti.sp-c-gMfcns:last-child { 31 | display: none; 32 | } 33 | 34 | .sp-tabs-scrollable-container { 35 | padding-left: 0 !important; 36 | } 37 | 38 | .sp-tab-button { 39 | border-right: 1px solid rgba(170, 167, 167, 0.2) !important; 40 | } 41 | 42 | .sp-console { 43 | border-top: 1px solid #efefef; 44 | background: white !important; 45 | --sp-syntax-color-plain: #4f4e4e !important; 46 | } 47 | 48 | .sp-c-gxughg::after { 49 | background: #efefef !important; 50 | } 51 | 52 | .cm-content.cm-readonly .cm-line { 53 | background: white !important; 54 | } 55 | 56 | .cm-gutterElement { 57 | font-size: 12px; 58 | } 59 | 60 | .dark { 61 | --sp-syntax-color-tag: #3ccaad !important; 62 | --sp-syntax-color-static: #3ccaad !important; 63 | --sp-syntax-color-keyword: #82b4e2 !important; 64 | --sp-colors-errorSurface: #fffefe !important; 65 | --selection-background: #fafafa; 66 | --selection-text-color: black; 67 | background: black; 68 | color: #a3a3a4; 69 | } 70 | 71 | .line-break-dark > h1, 72 | .line-break-dark > h2 { 73 | color: white; 74 | } 75 | 76 | .line-break > p > a { 77 | margin-left: 10px; 78 | color: #3ccaad !important; 79 | text-decoration: underline; 80 | } 81 | 82 | .next-link-dark, 83 | .prev-link-dark { 84 | color: white !important; 85 | } 86 | 87 | .dark-btn { 88 | color: rgb(215, 212, 212); 89 | } 90 | 91 | .dark-btn:hover { 92 | color: white; 93 | background: #0d3532 !important; 94 | } 95 | 96 | .shadow-dark { 97 | border-bottom: 1px solid #252525; 98 | } 99 | 100 | .dark-icon { 101 | color: rgb(215, 212, 212); 102 | width: 30px; 103 | height: 30px; 104 | } 105 | 106 | .light-icon { 107 | color: black; 108 | width: 30px; 109 | height: 30px; 110 | } 111 | 112 | .theme-switcher { 113 | width: 20px; 114 | height: 20px; 115 | cursor: pointer; 116 | } 117 | 118 | pre > div { 119 | border-radius: 8px; 120 | background-color: #252b37 !important; 121 | } 122 | 123 | pre > div > code { 124 | tab-size: 2 !important; 125 | } 126 | 127 | p > code { 128 | color: #3ccaad !important; 129 | background: rgba(196, 193, 193, 0.2); 130 | padding: 0px 5px 0px 5px; 131 | border-radius: 5px; 132 | } 133 | 134 | .language-js { 135 | font-size: 15px !important; 136 | } 137 | 138 | h1, 139 | h2 { 140 | font-weight: 400; 141 | } 142 | 143 | .line-break { 144 | white-space: pre-wrap; 145 | letter-spacing: -0.005em; 146 | font-weight: 300; 147 | font-size: 1.08rem; 148 | line-height: 1.3; 149 | margin-top: 0.25rem; 150 | } 151 | 152 | .header-cont { 153 | padding: 0 8px 0 8px; 154 | display: flex; 155 | justify-content: space-between; 156 | height: 60px; 157 | box-shadow: rgba(0, 0, 0, 0.15) 0px 2px 3px 0px; 158 | align-items: center; 159 | } 160 | 161 | .header-btn { 162 | font-size: 16px; 163 | padding: 5px; 164 | background: transparent; 165 | font-weight: 300; 166 | height: 45px; 167 | letter-spacing: 0.2px; 168 | border: none; 169 | outline: none; 170 | cursor: pointer; 171 | border-radius: 10px; 172 | margin: 5px; 173 | } 174 | 175 | /* support Samsung Galaxy S8+ */ 176 | @media screen and (max-width: 380px) { 177 | .header-btn { 178 | font-size: 14px; 179 | } 180 | } 181 | 182 | a { 183 | text-decoration: none; 184 | height: max-content; 185 | } 186 | 187 | .header-btn:hover { 188 | background: #dbe9fe; 189 | transition: 0.3s; 190 | } 191 | 192 | .text-black { 193 | cursor: pointer; 194 | position: absolute; 195 | right: 5%; 196 | max-height: 30px; 197 | } 198 | 199 | .defi { 200 | max-width: 200px; 201 | align-self: flex-end; 202 | margin-bottom: 6px; 203 | font-weight: 350; 204 | } 205 | 206 | .link-cont { 207 | display: flex; 208 | align-items: center; 209 | gap: 20px; 210 | } 211 | 212 | .home-btn { 213 | position: absolute; 214 | top: 63%; 215 | left: 53%; 216 | transform: translate(-53%, -53%); 217 | display: flex; 218 | gap: 20px; 219 | } 220 | 221 | .btn-1, 222 | .btn-2, 223 | .btn-3 { 224 | display: flex; 225 | justify-content: center; 226 | align-items: center; 227 | text-decoration: none; 228 | width: 120px; 229 | min-height: 50px; 230 | font-size: 17px; 231 | background: black; 232 | border: 2px solid black !important; 233 | color: white; 234 | border: none; 235 | outline: none; 236 | cursor: pointer; 237 | transition: 0.3s; 238 | } 239 | 240 | .home-svg { 241 | min-height: 100px; 242 | position: absolute; 243 | top: 45%; 244 | left: 51%; 245 | transform: translate(-50%, -49%); 246 | color: #d4d4d4; 247 | cursor: pointer; 248 | } 249 | 250 | .top-cont { 251 | display: flex; 252 | position: absolute; 253 | top: 5%; 254 | left: 3%; 255 | } 256 | 257 | .btn-1:hover { 258 | color: black; 259 | background-color: white; 260 | box-shadow: 5px 5px black; 261 | } 262 | 263 | .btn-2 { 264 | color: black; 265 | background-color: white; 266 | } 267 | 268 | .btn-2:hover { 269 | box-shadow: 5px 5px black; 270 | } 271 | 272 | .btn-3:hover { 273 | color: black; 274 | background-color: white; 275 | box-shadow: 5px 5px black; 276 | } 277 | 278 | .home-def { 279 | height: max-content; 280 | max-width: 250px; 281 | margin-top: 30px; 282 | background: #d4d4d4; 283 | border-radius: 10px; 284 | text-align: center; 285 | padding: 10px; 286 | } 287 | 288 | .home-def::before { 289 | border: solid transparent; 290 | border-right: solid #d4d4d4; 291 | border-width: 16px 24px 16px 0; 292 | border-left: 0; 293 | content: ""; 294 | margin-left: -36px; 295 | margin-top: -45px; 296 | height: 0; 297 | position: absolute; 298 | top: 50%; 299 | width: 0; 300 | } 301 | 302 | .lesson-cont { 303 | display: flex; 304 | height: calc(100vh - 60px); 305 | overflow-y: scroll; 306 | } 307 | 308 | @media screen and (max-width: 850px) { 309 | .lesson-cont { 310 | display: grid; 311 | } 312 | .mark-cont { 313 | height: 90vh !important; 314 | } 315 | .custom-layout { 316 | width: 100vw !important; 317 | } 318 | } 319 | 320 | .mark-cont { 321 | overflow-y: scroll; 322 | position: relative; 323 | display: flex; 324 | justify-content: space-between; 325 | flex-direction: column; 326 | gap: 20px; 327 | padding: 20px 20px 0 20px; 328 | } 329 | 330 | .pg-link { 331 | display: flex; 332 | justify-content: space-between; 333 | } 334 | 335 | .next-link, 336 | .prev-link { 337 | width: max-content; 338 | display: flex; 339 | align-items: flex-end; 340 | justify-content: flex-end; 341 | margin-bottom: 30px; 342 | font-size: 18px; 343 | color: black; 344 | } 345 | 346 | .next-link-intro { 347 | margin-left: auto; 348 | } 349 | 350 | .custom-layout { 351 | font-size: 15px; 352 | display: flex; 353 | flex-direction: column; 354 | height: calc(100vh - 60px); 355 | width: 62vw; 356 | } 357 | -------------------------------------------------------------------------------- /app/head.tsx: -------------------------------------------------------------------------------- 1 | export default function Head() { 2 | return ( 3 | <> 4 | Jotai tutorial 5 | 6 | 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import { Inter } from "@next/font/google"; 3 | const inter = Inter({ subsets: ["latin"] }); 4 | 5 | export default function RootLayout({ 6 | children, 7 | }: { 8 | children: React.ReactNode; 9 | }) { 10 | return ( 11 | 12 | 13 | {children} 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | 5 | export default function Home() { 6 | return ( 7 |
8 | 13 | Jotai 14 | 18 | 22 | 26 | 30 | 34 | 35 |
36 | 37 | Quick start 38 | 39 | 40 | Examples 41 | 42 | 43 | Playground 44 | 45 |
46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /app/playground/code.ts: -------------------------------------------------------------------------------- 1 | export const code = `import { atom, useAtom } from 'jotai'; 2 | 3 | const counter = atom(0); 4 | 5 | export default function Page() { 6 | const [count, setCounter] = useAtom(counter); 7 | const onClick = () => setCounter(prev => prev + 1); 8 | return ( 9 |
10 |

{count}

11 | 12 |
13 | ) 14 | }`; -------------------------------------------------------------------------------- /app/playground/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import './playground.css' 4 | import { 5 | SandpackProvider, 6 | SandpackLayout, 7 | SandpackCodeEditor, 8 | SandpackPreview, 9 | SandpackFileExplorer, 10 | useSandpack, 11 | } from "@codesandbox/sandpack-react"; 12 | import { useEffect, useState } from "react"; 13 | import { code } from './code' 14 | import { AiFillFileAdd, AiOutlineDelete, AiOutlineHome } from 'react-icons/ai' 15 | import { BsSun, BsFillMoonStarsFill } from "react-icons/bs" 16 | 17 | import { themeAtom } from "../quick-start/Header"; 18 | import { useAtom } from 'jotai'; 19 | 20 | const NewFile = () => { 21 | const [theme, setTheme] = useAtom(themeAtom); 22 | const { sandpack } = useSandpack(); 23 | const handleClick = () => { 24 | setTheme(); 25 | } 26 | 27 | useEffect(() => { 28 | sandpack.updateFile('/App.tsx', code) 29 | }, []) 30 | 31 | const fileAdd = () => { 32 | const newName = (window.prompt('Enter the new file name:')); 33 | sandpack.addFile(newName ? newName : 'Untitled') 34 | } 35 | 36 | const fileDelete = () => { 37 | const filepath = sandpack.activeFile 38 | console.log(filepath) 39 | sandpack.deleteFile(filepath) 40 | } 41 | 42 | return ( 43 | <> 44 |
45 | 46 | 49 | 50 | 53 | 56 |
57 | 58 | 70 | 71 |
72 | {theme === 'light' ? : } 73 |
74 | 75 | ) 76 | } 77 | 78 | function Page() { 79 | const [theme] = useAtom(themeAtom) 80 | const [file, setFile] = useState({}) 81 | return ( 82 |
83 | 93 |
94 | 95 |
96 | 97 | 101 | 102 | 103 | 104 | 105 |
106 |
107 | ) 108 | } 109 | 110 | export default Page -------------------------------------------------------------------------------- /app/playground/playground.css: -------------------------------------------------------------------------------- 1 | .sp-c-bNbSGz { 2 | font-size: 15px; 3 | } 4 | 5 | .sp-layout { 6 | border-radius: 0px !important; 7 | } 8 | 9 | .sp-close-button { 10 | margin-bottom: 8px; 11 | } 12 | 13 | .sp-file-explorer { 14 | overflow: scroll !important; 15 | } 16 | 17 | .left-dark-btn { 18 | background: black !important; 19 | color: white; 20 | } 21 | 22 | .dark-mid-btn { 23 | background: black !important; 24 | color: white; 25 | } 26 | 27 | .dark-mid-btn:hover { 28 | background: #0d3532 !important; 29 | } 30 | 31 | .left-dark-btn:hover { 32 | background: #0d3532 !important; 33 | } 34 | 35 | .newFile-light { 36 | height: 55px; 37 | display: flex; 38 | justify-content: space-between; 39 | align-items: center; 40 | } 41 | 42 | .btn { 43 | background: white; 44 | height: 55px; 45 | width: 70px; 46 | border: none; 47 | outline: none; 48 | cursor: pointer; 49 | } 50 | 51 | .newFile-dark { 52 | background: black; 53 | color: white !important; 54 | height: 55px; 55 | display: flex; 56 | justify-content: space-between; 57 | align-items: center; 58 | } 59 | 60 | .playground-btn { 61 | font-size: 15px; 62 | padding: 5px; 63 | background: transparent; 64 | font-weight: 300; 65 | height: 35px; 66 | letter-spacing: 0.2px; 67 | border: none; 68 | outline: none; 69 | cursor: pointer; 70 | border-radius: 10px; 71 | margin: 5px; 72 | } 73 | 74 | .playground-mid { 75 | margin-left: -130px; 76 | } 77 | 78 | .playground-toogle { 79 | margin-right: 20px; 80 | } 81 | 82 | .playground-btn:hover { 83 | background: #dbe9fe; 84 | transition: 0.3s; 85 | } 86 | 87 | a { 88 | text-decoration: none; 89 | color: black; 90 | height: inherit; 91 | } 92 | 93 | .btn:hover { 94 | background: black; 95 | color: white; 96 | transition: 0.3s; 97 | } 98 | 99 | .icon { 100 | height: 20px; 101 | width: 20px; 102 | } 103 | 104 | @media screen and (min-width: 900px) { 105 | .sp-c-ctabmA { 106 | overflow-y: scroll; 107 | } 108 | .sp-stack, .sp-file-explorer { 109 | height:calc(100vh - 55px) !important; 110 | overflow-y: scroll; 111 | } 112 | } 113 | 114 | @media screen and (max-width: 900px) { 115 | .sp-c-ctabmA > .sp-file-explorer { 116 | height: 100vh; 117 | overflow-y: scroll; 118 | } 119 | } -------------------------------------------------------------------------------- /app/quick-start/Hamburger.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './hamburger.css' 3 | 4 | import { themeAtom } from "./Header"; 5 | import { useAtom } from 'jotai'; 6 | 7 | function Hamburger() { 8 | const [theme] = useAtom(themeAtom) 9 | return ( 10 |
11 | 12 | 15 | 16 | 29 |
30 | ) 31 | } 32 | 33 | export default Hamburger -------------------------------------------------------------------------------- /app/quick-start/Header.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React from "react"; 4 | import Hamburger from "./Hamburger"; 5 | import { AiFillGithub } from "react-icons/ai"; 6 | import { atom, useAtom } from "jotai"; 7 | import { BsSun, BsFillMoonStarsFill } from "react-icons/bs"; 8 | 9 | const theme = atom('light'); 10 | export const themeAtom = atom(get => get(theme), (get, set) => { 11 | set(theme, get(theme) === 'light'? 'dark': 'light') 12 | }); 13 | 14 | function Header() { 15 | const [appTheme, setTheme] = useAtom(theme); 16 | const handleClick = () => { 17 | setTheme(appTheme === 'light'? 'dark': 'light'); 18 | } 19 | return ( 20 |
21 | 22 | 23 | 40 | 41 |
42 | {appTheme === 'light'? : } 43 | 44 | 45 | 46 |
47 |
48 | ); 49 | } 50 | 51 | export default Header; 52 | -------------------------------------------------------------------------------- /app/quick-start/async-read-atoms/code.ts: -------------------------------------------------------------------------------- 1 | const code = `import { atom, useAtom } from 'jotai'; 2 | import { Suspense } from 'react' 3 | 4 | const counter = atom(1); 5 | const asyncAtom = atom(async (get) => get(counter) * 5); 6 | 7 | function AsyncComponent() { 8 | const [asyncCount] = useAtom(asyncAtom); 9 | return ( 10 |
11 |

{asyncCount}

12 |
13 | ) 14 | } 15 | 16 | export default function Page() { 17 | return ( 18 | loading...}> 19 | 20 | 21 | ) 22 | } 23 | `; 24 | 25 | const files = { 26 | "/App.js": { 27 | code: code, 28 | }, 29 | }; 30 | 31 | export default files; 32 | -------------------------------------------------------------------------------- /app/quick-start/async-read-atoms/markdown.ts: -------------------------------------------------------------------------------- 1 | export const markdown = ` 2 | # Async Read Atoms 3 | 4 | Using async atoms, you gain access to real-world data while still managing them directly from your atoms and with incredible ease. 5 | 6 | We separate async atoms in two main categories: 7 | •Async read atoms 8 | •Async write atoms 9 | 10 | Let's see first the async read atoms. 11 | The \`read\` function of an atom can return a promise. 12 | ~~~js 13 | const counter = atom(0); 14 | const asyncAtom = atom(async (get) => get(counter) * 5); 15 | ~~~ 16 | 17 | Jotai is inherently leveraging \`Suspense\` to handle asynchronous flows. 18 | 19 | ~~~js 20 | loading...}> 21 | 22 | 23 | ~~~ 24 | 25 | But there is a more jotai way of doing this with the \`loadable api\` present in \`jotai/utils\`. By simply wrapping the atom in loadable util and it returns the value with one of the three states: \`loading\`, \`hasData\` and \`hasError\`. 26 | 27 | ~~~js 28 | { 29 | state: 'loading' | 'hasData' | 'hasError', 30 | data?: any, 31 | error?: any, 32 | } 33 | ~~~ 34 | 35 | ~~~js 36 | import { loadable } from "jotai/utils" 37 | 38 | const countAtom = atom(0); 39 | const asyncAtom = atom(async (get) => get(countAtom)); 40 | const loadableAtom = loadable(asyncAtom) 41 | const AsyncComponent = () => { 42 | const [value] = useAtom(loadableAtom) 43 | if (value.state === 'hasError') return
{value.error}
44 | if (value.state === 'loading') { 45 | return
Loading...
46 | } 47 | return
Value: {value.data}
48 | ~~~ 49 | `; 50 | -------------------------------------------------------------------------------- /app/quick-start/async-read-atoms/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { Sandpack } from "@codesandbox/sandpack-react"; 5 | import Markdown from "react-markdown"; 6 | import { markdown } from "./markdown"; 7 | import files from "./code"; 8 | import setupStyles from "./styles"; 9 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; 10 | import { vscDarkPlus } from "../../vscDarkPlus"; 11 | import Link from "next/link"; 12 | 13 | import { themeAtom } from "../Header"; 14 | import { useAtom } from 'jotai'; 15 | 16 | function Page() { 17 | const [theme] = useAtom(themeAtom) 18 | return ( 19 |
20 |
21 | 33 | {String(children).replace(/\n$/, "")} 34 | 35 | ) : ( 36 | 37 | {children} 38 | 39 | ); 40 | }, 41 | }} 42 | > 43 | {markdown} 44 | 45 |
46 | 47 | {"<-"} Prev 48 | 49 | 50 | Next {"->"} 51 | 52 |
53 |
54 | 55 | 78 |
79 | ); 80 | } 81 | 82 | export default Page; 83 | -------------------------------------------------------------------------------- /app/quick-start/async-read-atoms/styles.ts: -------------------------------------------------------------------------------- 1 | const StylesCss = ` 2 | body { 3 | font-family: sans-serif; 4 | font-weight: normal; 5 | } 6 | .app { 7 | width: 100vw; 8 | } 9 | `; 10 | 11 | const setupStyles = { 12 | "/styles.css": { 13 | code: StylesCss, 14 | hidden: true, 15 | }, 16 | }; 17 | 18 | export default setupStyles; 19 | -------------------------------------------------------------------------------- /app/quick-start/async-write-atoms/code.ts: -------------------------------------------------------------------------------- 1 | const code1 = `import { atom, useAtom } from 'jotai'; 2 | import Suspense from './Suspense.js'; 3 | 4 | const counter = atom(1); 5 | 6 | const asyncAtom = atom(null, async (get, set) => { 7 | await fetch('https://jsonplaceholder.typicode.com/todos/'); 8 | set(counter, get(counter) + 1); 9 | }); 10 | 11 | function AsyncComponent() { 12 | const [count] = useAtom(counter); 13 | const [, incCounter] = useAtom(asyncAtom); 14 | return ( 15 |
16 |

{count}

17 | 18 |
19 | ) 20 | } 21 | 22 | export default function Page() { 23 | return ( 24 |
25 | 26 | 27 |
28 | ) 29 | } 30 | `; 31 | 32 | const code2 = `import { atom, useAtom } from 'jotai'; 33 | import { Suspense } from 'react' 34 | 35 | const todo = { 36 | id: 0, 37 | title: 'learn jotai', 38 | completed: true 39 | }; 40 | 41 | const request = async () => ( 42 | fetch('https://jsonplaceholder.typicode.com/todos/5') 43 | .then((res) => res.json()) 44 | ) 45 | const todoAtom = atom(todo); 46 | 47 | function Component() { 48 | const [todoGoal, setGoal] = useAtom(todoAtom); 49 | const handleClick = () => { 50 | setGoal(request()); 51 | } 52 | return ( 53 |
54 |

Todays Goal: {todoGoal.title}

55 | 56 |
57 | ) 58 | } 59 | 60 | export default function AsyncSuspense() { 61 | return ( 62 |
63 | loading...}> 64 | 65 | 66 |
67 | ) 68 | } 69 | `; 70 | 71 | const files = { 72 | "/App.js": { 73 | code: code1, 74 | }, 75 | "/Suspense.js": { 76 | code: code2, 77 | }, 78 | }; 79 | 80 | export default files; 81 | -------------------------------------------------------------------------------- /app/quick-start/async-write-atoms/markdown.ts: -------------------------------------------------------------------------------- 1 | export const markdown = ` 2 | # Async Write Atoms 3 | 4 | In Async write atoms the \`write\` function of atom returns a promise. 5 | 6 | ~~~js 7 | const counter = atom(0); 8 | const asyncAtom = atom(null, async (set, get) => { 9 | // await something 10 | set(counter, get(counter) + 1); 11 | }); 12 | ~~~ 13 | 14 | **Note**: An important take here is that async write function does not trigger the Suspense. 15 | 16 | But an interesting pattern that can be achieved with Jotai is switching from async to sync to trigger suspending when wanted. 17 | 18 | ~~~js 19 | const request = async () => fetch('https://...').then((res) => res.json()) 20 | const baseAtom = atom(0) 21 | const Component = () => { 22 | const [value, setValue] = useAtom(baseAtom) 23 | const handleClick = () => { 24 | setValue(request()) // Will suspend until request resolves 25 | } 26 | // ... 27 | } 28 | ~~~ 29 | `; 30 | -------------------------------------------------------------------------------- /app/quick-start/async-write-atoms/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { Sandpack } from "@codesandbox/sandpack-react"; 5 | import Markdown from "react-markdown"; 6 | import { markdown } from "./markdown"; 7 | import files from "./code"; 8 | import setupStyles from "./styles"; 9 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; 10 | import { vscDarkPlus } from "../../vscDarkPlus"; 11 | import Link from "next/link"; 12 | 13 | import { themeAtom } from "../Header"; 14 | import { useAtom } from 'jotai'; 15 | 16 | 17 | function Page() { 18 | const [theme] = useAtom(themeAtom) 19 | return ( 20 |
21 |
22 | 34 | {String(children).replace(/\n$/, "")} 35 | 36 | ) : ( 37 | 38 | {children} 39 | 40 | ); 41 | }, 42 | }} 43 | > 44 | {markdown} 45 | 46 |
47 | 48 | {"<-"} Prev 49 | 50 | 51 | Next {"->"} 52 | 53 |
54 |
55 | 56 | 79 |
80 | ); 81 | } 82 | 83 | export default Page; 84 | -------------------------------------------------------------------------------- /app/quick-start/async-write-atoms/styles.ts: -------------------------------------------------------------------------------- 1 | const StylesCss = ` 2 | body { 3 | font-family: sans-serif; 4 | font-weight: normal; 5 | } 6 | .app { 7 | width: 100vw; 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: center; 11 | align-items: center; 12 | } 13 | button { 14 | // margin-left: -40px; 15 | height: 45px; 16 | width: 100px; 17 | border: 2px solid #4f4e4e; 18 | cursor: pointer; 19 | font-size: 17px; 20 | } 21 | `; 22 | 23 | const setupStyles = { 24 | "/styles.css": { 25 | code: StylesCss, 26 | hidden: true, 27 | }, 28 | }; 29 | 30 | export default setupStyles; 31 | -------------------------------------------------------------------------------- /app/quick-start/atom-creators/code.ts: -------------------------------------------------------------------------------- 1 | const code = `import { atom, useAtom } from 'jotai' 2 | 3 | const createCountIncAtoms = (initialValue) => { 4 | const baseAtom = atom(initialValue) 5 | const valueAtom = atom((get) => get(baseAtom)) 6 | const incAtom = atom(null, (get, set) => set(baseAtom, (c) => c + 1)) 7 | return [valueAtom, incAtom] 8 | } 9 | 10 | const [fooAtom, fooIncAtom] = createCountIncAtoms(0) 11 | const [barAtom, barIncAtom] = createCountIncAtoms(0) 12 | 13 | function App() { 14 | const [fooCount] = useAtom(fooAtom) 15 | const [, incFoo] = useAtom(fooIncAtom) 16 | const [barCount] = useAtom(barAtom) 17 | const [, incBar] = useAtom(barIncAtom) 18 | 19 | const onClick1 = () => { 20 | incFoo() 21 | } 22 | 23 | const onClick2 = () => { 24 | incBar() 25 | } 26 | 27 | return ( 28 |
29 |
30 | {fooCount} 31 | 32 |
33 |
34 | {barCount} 35 | 36 |
37 |
38 | ) 39 | } 40 | 41 | export default App`; 42 | 43 | const files = { 44 | "/App.js": { 45 | code: code, 46 | }, 47 | }; 48 | 49 | export default files; 50 | -------------------------------------------------------------------------------- /app/quick-start/atom-creators/markdown.ts: -------------------------------------------------------------------------------- 1 | export const markdown = ` 2 | # Atom Creators 3 | 4 | An atom creator means simply a function that returns an atom or a set of atoms. It's just a function and it's not some features that the library provides, but it's an important pattern to make a fairly complex use case. This avoids the boilerplate of having to set up another atom just to update the state of the first. 5 | 6 | Consider this case, 7 | ~~~js 8 | const fooAtom = atom(0); 9 | const barAtom = atom(0); 10 | const incFooAtom = atom(null, (get, set) => { 11 | set(fooAtom, c => c + 1); 12 | }; 13 | const incBarAtom = atom(null, (get, set) => { 14 | set(barAtom, c => c + 1); 15 | }; 16 | ~~~ 17 | Although you can attach the suitable actions to the setter of the respective atom, but this also increases boilerplate code when there are more atoms in your code. 18 | ~~~js 19 | const incAllAtom = atom(null, (get, set, action) => { 20 | if(action === 'inc1') // increase first atom 21 | if(action === 'inc2') // increase second atom 22 | ... 23 | } 24 | ~~~ 25 | 26 | So simply replace this with the atom creators function. 27 | ~~~js 28 | const createCountIncAtoms = (initialValue) => { 29 | const baseAtom = atom(initialValue) 30 | const valueAtom = atom((get) => get(baseAtom)) 31 | const incAtom = atom(null, (get, set) => set(baseAtom, (c) => c + 1)) 32 | return [valueAtom, incAtom] 33 | } 34 | ~~~ 35 | ` -------------------------------------------------------------------------------- /app/quick-start/atom-creators/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { Sandpack } from "@codesandbox/sandpack-react"; 5 | import Markdown from "react-markdown"; 6 | import { markdown } from "./markdown"; 7 | import files from "./code"; 8 | import setupStyles from "./styles"; 9 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; 10 | import { vscDarkPlus } from "../../vscDarkPlus"; 11 | import Link from "next/link"; 12 | 13 | import { themeAtom } from "../Header"; 14 | import { useAtom } from 'jotai'; 15 | 16 | 17 | function Page() { 18 | const [theme] = useAtom(themeAtom) 19 | return ( 20 |
21 |
22 | 34 | {String(children).replace(/\n$/, "")} 35 | 36 | ) : ( 37 | 38 | {children} 39 | 40 | ); 41 | }, 42 | }} 43 | > 44 | {markdown} 45 | 46 |
47 | 48 | {"<-"} Prev 49 | 50 | 51 | Next {"->"} 52 | 53 |
54 |
55 | 56 | 79 |
80 | ); 81 | } 82 | 83 | export default Page; 84 | -------------------------------------------------------------------------------- /app/quick-start/atom-creators/styles.ts: -------------------------------------------------------------------------------- 1 | const StylesCss = ` 2 | body { 3 | font-family: sans-serif; 4 | font-weight: normal; 5 | } 6 | .app { 7 | width: 100vw; 8 | height: 80vh; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | gap: 20px; 14 | } 15 | span { 16 | font-size: 25px; 17 | } 18 | button { 19 | margin-left: 10px; 20 | height: 45px; 21 | width: 80px; 22 | border: 2px solid #4f4e4e; 23 | cursor: pointer; 24 | font-size: 17px; 25 | } 26 | `; 27 | 28 | const setupStyles = { 29 | "/styles.css": { 30 | code: StylesCss, 31 | hidden: true, 32 | }, 33 | }; 34 | 35 | export default setupStyles; 36 | -------------------------------------------------------------------------------- /app/quick-start/hamburger.css: -------------------------------------------------------------------------------- 1 | .hamburger-menu { 2 | z-index: 1; 3 | } 4 | 5 | .dark-menu { 6 | background: black !important; 7 | border-right: 1.5px solid #252525; 8 | } 9 | 10 | .dark-item { 11 | color: rgb(212, 206, 206) !important; 12 | } 13 | 14 | .dark-item:hover { 15 | background: #0d3532 !important; 16 | } 17 | 18 | #menu__toggle { 19 | opacity: 0; 20 | width: 30px; 21 | height: 25px; 22 | margin-left: 3px; 23 | cursor: pointer; 24 | } 25 | 26 | span { 27 | margin-top: 8px; 28 | } 29 | 30 | #menu__toggle:checked+.menu__btn>span { 31 | transform: rotate(45deg); 32 | } 33 | 34 | #menu__toggle:checked+.menu__btn>span::before { 35 | top: 0; 36 | transform: rotate(0deg); 37 | } 38 | 39 | #menu__toggle:checked+.menu__btn>span::after { 40 | top: 0; 41 | transform: rotate(90deg); 42 | } 43 | 44 | #menu__toggle:checked~.menu__box { 45 | left: 0 !important; 46 | } 47 | 48 | .menu__btn { 49 | position: absolute; 50 | top: 20px; 51 | left: 20px; 52 | width: 26px; 53 | height: 20px; 54 | cursor: pointer; 55 | z-index: 11; 56 | } 57 | 58 | .menu__btn>span, 59 | .menu__btn>span::before, 60 | .menu__btn>span::after { 61 | display: block; 62 | position: absolute; 63 | width: 100%; 64 | height: 2px; 65 | background-color: #616161; 66 | transition-duration: .25s; 67 | } 68 | 69 | .menu__btn>span::before { 70 | content: ''; 71 | top: -8px; 72 | } 73 | 74 | .menu__btn>span::after { 75 | content: ''; 76 | top: 8px; 77 | } 78 | 79 | .menu__box { 80 | display: block; 81 | position: fixed; 82 | top: 0; 83 | left: -100%; 84 | width: 230px; 85 | height: 100%; 86 | margin: 0; 87 | padding: 60px 0; 88 | list-style: none; 89 | background-color: #ECEFF1; 90 | box-shadow: 2px 2px 6px rgba(37, 36, 36, 0.4); 91 | transition-duration: .25s; 92 | } 93 | 94 | .menu__item { 95 | display: block; 96 | padding: 8px 20px; 97 | color: black; 98 | font-size: 16px; 99 | font-weight: 300; 100 | text-decoration: none; 101 | transition-duration: .3s; 102 | } 103 | 104 | .menu__item:hover { 105 | background-color: #dbe9fe; 106 | } 107 | 108 | a, a>li { 109 | text-decoration: none; 110 | } -------------------------------------------------------------------------------- /app/quick-start/immer-integration/code.ts: -------------------------------------------------------------------------------- 1 | const code = `import { useAtom } from 'jotai'; 2 | import { atomWithImmer } from 'jotai-immer'; 3 | 4 | const todo = { 5 | todo: { 6 | person: { 7 | name: "David", 8 | title: { 9 | goal: "old todo" 10 | }, 11 | } 12 | } 13 | }; 14 | const immerAtom = atomWithImmer(todo); 15 | 16 | export default function Page() { 17 | const [todoAtom, setAtomTodo] = useAtom(immerAtom); 18 | const updateTodo = () => { 19 | setAtomTodo(state => { 20 | state.todo.person.title.goal = "new title"; 21 | return state; 22 | }); 23 | } 24 | return ( 25 |
26 |

Name: {todoAtom.todo.person.name}

27 |

Todo: {todoAtom.todo.person.title.goal}

28 | 29 |
30 | ) 31 | }`; 32 | 33 | const files = { 34 | "/App.js": { 35 | code: code, 36 | }, 37 | }; 38 | 39 | export default files; 40 | -------------------------------------------------------------------------------- /app/quick-start/immer-integration/markdown.ts: -------------------------------------------------------------------------------- 1 | export const markdown = ` 2 | # Integrations 3 | 4 | Updating the state with jotai is simple with the provided \`set\` function but things can go complex and requires some extra effort with the nested object states as you have to copy the state at each level with the spread operator \`...\` like so, 5 | ~~~js 6 | ... 7 | setAtomTodo(state => { 8 | const deepCopyState = { 9 | ...state, 10 | todo: { 11 | ...state.todo, 12 | person: { 13 | ...state.todo.person, 14 | title: { 15 | ...state.todo.person.title, 16 | goal: "new title" 17 | } 18 | } 19 | } 20 | } 21 | return deepCopyState; 22 | }); 23 | ~~~ 24 | 25 | This is a very naive method and there may be a higher chance that you make some mistakes while updating the state like this. 26 | 27 | To make our life easy we can take advantage of jotai 3rd party library's support. Jotai officially supports \`Immer\`, \`Optics\`, \`Zustand\`, \`tRPC\`, and various other 3rd party integrations. 28 | 29 | Let's see how we can use \`immer\` to directly mutate the state, 30 | You have to install \`immer\` and \`jotai-immer\` to use this feature. 31 | ~~~js 32 | npm install immer jotai-immer 33 | ~~~ 34 | Create a new atom with \`atomWithImmer\`. 35 | ~~~js 36 | import { atomWithImmer } from 'jotai-immer'; 37 | 38 | const immerAtom = atomWithImmer(todo); 39 | ... 40 | const updateTodo = () => { 41 | setAtomTodo(immerTodo => { 42 | // directly mutating the state with immer 43 | immerTodo.todo.person.title = "new title"; 44 | return immerTodo; 45 | }); 46 | } 47 | ~~~ 48 | 49 | \`atomWithImmer\` creates a new atom similar to the regular \`atom\` with a different writeFunction. In this bundle, we don't have read-only atoms, because the point of these functions is the immer produce(mutability) function. The signature of writeFunction is \`(get, set, update: (draft: Draft) => void) => void\`. 50 | ` 51 | -------------------------------------------------------------------------------- /app/quick-start/immer-integration/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { Sandpack } from "@codesandbox/sandpack-react"; 5 | import Markdown from "react-markdown"; 6 | import { markdown } from "./markdown"; 7 | import files from "./code"; 8 | import setupStyles from "./styles"; 9 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; 10 | import { vscDarkPlus } from "../../vscDarkPlus"; 11 | import Link from "next/link"; 12 | 13 | import { themeAtom } from "../Header"; 14 | import { useAtom } from 'jotai'; 15 | 16 | 17 | function Page() { 18 | const [theme] = useAtom(themeAtom) 19 | return ( 20 |
21 |
22 | 34 | {String(children).replace(/\n$/, "")} 35 | 36 | ) : ( 37 | 38 | {children} 39 | 40 | ); 41 | }, 42 | }} 43 | > 44 | {markdown} 45 | 46 |
47 | 48 | {"<-"} Prev 49 | 50 |
51 |
52 | 53 | 78 |
79 | ); 80 | } 81 | 82 | export default Page; 83 | -------------------------------------------------------------------------------- /app/quick-start/immer-integration/styles.ts: -------------------------------------------------------------------------------- 1 | const StylesCss = ` 2 | body { 3 | font-family: sans-serif; 4 | font-weight: normal; 5 | } 6 | .app { 7 | width: 100vw; 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: center; 11 | align-items: center; 12 | } 13 | h3 { 14 | margin-bottom: 0px; 15 | } 16 | button { 17 | height: 45px; 18 | width: max-content; 19 | border: 2px solid #4f4e4e; 20 | cursor: pointer; 21 | font-size: 17px; 22 | } 23 | `; 24 | 25 | const setupStyles = { 26 | "/styles.css": { 27 | code: StylesCss, 28 | hidden: true, 29 | }, 30 | }; 31 | 32 | export default setupStyles; 33 | -------------------------------------------------------------------------------- /app/quick-start/intro/code.ts: -------------------------------------------------------------------------------- 1 | const code = `import { atom, useAtom } from 'jotai'; 2 | 3 | const counter = atom(0); 4 | 5 | export default function Page() { 6 | const [count, setCounter] = useAtom(counter); 7 | const onClick = () => setCounter(prev => prev + 1); 8 | return ( 9 |
10 |

{count}

11 | 12 |
13 | ) 14 | }`; 15 | 16 | const files = { 17 | "/App.js": { 18 | code: code, 19 | }, 20 | }; 21 | 22 | export default files; 23 | -------------------------------------------------------------------------------- /app/quick-start/intro/markdown.ts: -------------------------------------------------------------------------------- 1 | export const markdown = ` 2 | # Creating your first atom 3 | 4 | Jotai atoms are small isolated pieces of state. Ideally, one atom contains very small data. 5 | Here's how you create your first atom. 6 | 7 | ~~~js 8 | import { atom } from 'jotai'; 9 | const counter = atom(0); 10 | ~~~ 11 | 12 | It is as simple to use as React’s integrated \`useState\` hook, but all state is globally accessible. 13 | 14 | ~~~js 15 | const [count, setCounter] = useAtom(counter); 16 | ~~~ 17 | 18 | The atom we created is to be passed to \`useState\` hook with the help of jotai \`useAtom\` function, which returns an array, where the 1st element is the value of atom, and the 2nd element is a function used to set the value of the atom. 19 | 20 | Jotai considers anything to be an atom so you can create any type of atom you want whether it is atom of objects, arrays, or nested objects. 21 | ~~~js 22 | const friendObj = atom({ name: "John", online: false }); 23 | const cities = atom([ "Tokoyo", "Kyoto", "Osaka" ]); 24 | const nestedObj = atom({ friend1: { name: "John", age: 18 } }); 25 | ~~~ 26 | `; 27 | -------------------------------------------------------------------------------- /app/quick-start/intro/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { Sandpack } from "@codesandbox/sandpack-react"; 5 | import Markdown from "react-markdown"; 6 | import { markdown } from "./markdown"; 7 | import files from "./code"; 8 | import setupStyles from "./styles"; 9 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; 10 | import { vscDarkPlus } from "../../vscDarkPlus"; 11 | import Link from "next/link"; 12 | 13 | import { themeAtom } from "../Header"; 14 | import { useAtom } from 'jotai'; 15 | 16 | 17 | function Page() { 18 | const [theme] = useAtom(themeAtom) 19 | return ( 20 |
21 |
22 | 34 | {String(children).replace(/\n$/, "")} 35 | 36 | ) : ( 37 | 38 | {children} 39 | 40 | ); 41 | }, 42 | }} 43 | > 44 | {markdown} 45 | 46 |
47 |
48 | 49 | Next {"->"} 50 | 51 |
52 |
53 | 54 | 77 |
78 | ); 79 | } 80 | 81 | export default Page; 82 | -------------------------------------------------------------------------------- /app/quick-start/intro/styles.ts: -------------------------------------------------------------------------------- 1 | const StylesCss = ` 2 | body { 3 | width: 100vw; 4 | font-family: sans-serif; 5 | font-weight: normal; 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | } 10 | 11 | button { 12 | margin-left: -40px; 13 | height: 45px; 14 | width: 100px; 15 | border: 2px solid #4f4e4e; 16 | cursor: pointer; 17 | font-size: 17px; 18 | } 19 | `; 20 | 21 | const setupStyles = { 22 | "/styles.css": { 23 | code: StylesCss, 24 | hidden: true, 25 | }, 26 | }; 27 | 28 | export default setupStyles; 29 | -------------------------------------------------------------------------------- /app/quick-start/layout.tsx: -------------------------------------------------------------------------------- 1 | import "../globals.css"; 2 | import { Inter } from "@next/font/google"; 3 | import Header from "./Header"; 4 | const inter = Inter({ subsets: ["latin"] }); 5 | 6 | export default function RootLayout({ 7 | children, 8 | }: { 9 | children: React.ReactNode; 10 | }) { 11 | return ( 12 | 13 | 14 | 15 |
16 | {children} 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /app/quick-start/official-utils/code.ts: -------------------------------------------------------------------------------- 1 | const code1 = `import AtomWithReset from './AtomWithReset.js' 2 | import SelectAtom from './SelectAtom.js' 3 | 4 | export default function App() { 5 | return ( 6 |
7 | 8 | 9 |
10 | ) 11 | } 12 | ` 13 | 14 | const code2 = `import { useAtom } from 'jotai'; 15 | import { atomWithReset, useResetAtom } from 'jotai/utils'; 16 | 17 | const counter = atomWithReset(1); 18 | 19 | export default function Counter() { 20 | const [count, setCount] = useAtom(counter); 21 | const reset = useResetAtom(counter); 22 | 23 | const inc = () => setCount(c => c * 2); 24 | 25 | return ( 26 |
27 |

1. atomWithReset

28 |

{count}

29 |
30 | 31 | 32 |
33 |
34 | ); 35 | } 36 | ` 37 | 38 | const code3 = `import { atom, Provider, useAtom } from "jotai"; 39 | import { selectAtom } from "jotai/utils"; 40 | import { useRef, useEffect } from "react"; 41 | import { isEqual } from 'lodash-es'; 42 | 43 | const defaultPerson = { 44 | name: { 45 | first: "Jane", 46 | last: "Doe" 47 | }, 48 | birth: { 49 | year: 2000, 50 | month: "Jan", 51 | day: 1, 52 | time: { 53 | hour: 0, 54 | minute: 0 55 | } 56 | } 57 | }; 58 | 59 | // Original atom. 60 | const personAtom = atom(defaultPerson); 61 | 62 | // Tracks person.name. Updated when person.name object changes, even 63 | // if neither name.first nor name.last actually change. 64 | const nameAtom = selectAtom(personAtom, (person) => person.name); 65 | 66 | // Tracks person.birth. Updated when year, month, day, hour, or minute changes. 67 | // Use of deepEquals means that this atom doesn't update if birth field is 68 | // replaced with a new object containing the same data. E.g., if person is re-read 69 | // from a database. 70 | const birthAtom = selectAtom(personAtom, (person) => person.birth, isEqual); 71 | 72 | const useCommitCount = () => { 73 | const rerenderCountRef = useRef(0); 74 | useEffect(() => { 75 | rerenderCountRef.current += 1; 76 | }); 77 | return rerenderCountRef.current; 78 | }; 79 | 80 | // Rerenders when nameAtom changes. 81 | const DisplayName = () => { 82 | const [name] = useAtom(nameAtom); 83 | const n = useCommitCount(); 84 | return ( 85 |
86 | Name: {name.first} {name.last}: re-rendered {n} times 87 |
88 | ); 89 | }; 90 | 91 | // Re-renders when birthAtom changes. 92 | const DisplayBirthday = () => { 93 | const [birth] = useAtom(birthAtom); 94 | const n = useCommitCount(); 95 | return ( 96 |
97 | Birthday: 98 | {birth.month}/{birth.day}/{birth.year}: (re-rendered {n} times) 99 |
100 | ); 101 | }; 102 | 103 | // Swap first and last names, triggering a change in nameAtom, but 104 | // not in birthAtom. 105 | const SwapNames = () => { 106 | const [person, setPerson] = useAtom(personAtom); 107 | const handleChange = () => { 108 | setPerson({ 109 | ...person, 110 | name: { first: person.name.last, last: person.name.first } 111 | }); 112 | }; 113 | return ; 114 | }; 115 | 116 | // Replace person with a deep copy, triggering a change in nameAtom, but 117 | // not in birthAtom. 118 | const CopyPerson = () => { 119 | const [person, setPerson] = useAtom(personAtom); 120 | const handleClick = () => { 121 | setPerson({ 122 | name: { first: person.name.first, last: person.name.last }, 123 | birth: { 124 | year: person.birth.year, 125 | month: person.birth.month, 126 | day: person.birth.day, 127 | time: { 128 | hour: person.birth.time.hour, 129 | minute: person.birth.time.minute 130 | } 131 | } 132 | }); 133 | }; 134 | return ; 135 | }; 136 | 137 | // Changes birth year, triggering a change to birthAtom, but not nameAtom. 138 | const IncrementBirthYear = () => { 139 | const [person, setPerson] = useAtom(personAtom); 140 | const handleClick = () => { 141 | setPerson({ 142 | name: person.name, 143 | birth: { ...person.birth, year: person.birth.year + 1 } 144 | }); 145 | }; 146 | return ; 147 | }; 148 | 149 | export default function App() { 150 | return ( 151 |
152 |

2. selectAtom

153 | 154 | 155 | 156 |
157 | 158 | 159 | 160 |
161 |
162 |
163 | ); 164 | } 165 | ` 166 | ; 167 | 168 | const files = { 169 | "/App.js": { 170 | code: code1, 171 | }, 172 | "/AtomWithReset.js": { 173 | code: code2 174 | }, 175 | "/SelectAtom.js": { 176 | code: code3 177 | } 178 | }; 179 | 180 | export default files; 181 | -------------------------------------------------------------------------------- /app/quick-start/official-utils/markdown.ts: -------------------------------------------------------------------------------- 1 | export const markdown = ` 2 | # Utils 3 | This is an overview of the atom creators/hooks utilities that can be found under \`jotai/utils\`. 4 | We already covered \`atomWithStorage\` and \`loadable\` API in previous lessons. 5 | 6 | 1.[atomWithReset](https://jotai.org/docs/utils/atom-with-reset) 7 | Creates an atom that could be reset to its initialValue with \`useResetAtom\` hook. It works exactly the same way as primitive atom would, but you are also able to set it to a special value [\`RESET\`](https://jotai.org/docs/utils/reset). 8 | ~~~js 9 | import { atomWithReset } from 'jotai/utils' 10 | 11 | const counter = atomWithReset(1) 12 | ~~~ 13 | 14 | 2.[selectAtom](https://jotai.org/docs/utils/select-atom) 15 | This function creates a derived atom whose value is a function of the original atom's value, determined by \`selector\`. The selector function runs whenever the original atom changes; it updates the derived atom only if \`equalityFn\` reports that the derived value has changed. By default, \`equalityFn\` is reference equality, but you can supply your favorite deep-equals function to stabilize the derived value where necessary. 16 | ~~~js 17 | const defaultPerson = { 18 | name: { 19 | first: 'Jane', 20 | last: 'Doe', 21 | }, 22 | birth: { 23 | year: 2000, 24 | month: 'Jan', 25 | day: 1, 26 | } 27 | } 28 | 29 | // Original atom. 30 | const personAtom = atom(defaultPerson) 31 | const nameAtom = selectAtom(personAtom, (person) => person.name, deepEqual) 32 | ~~~ 33 | 34 | Read [docs](https://jotai.org/docs/api/utils#overview) for more utils. 35 | ` -------------------------------------------------------------------------------- /app/quick-start/official-utils/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { Sandpack } from "@codesandbox/sandpack-react"; 5 | import Markdown from "react-markdown"; 6 | import { markdown } from "./markdown"; 7 | import files from "./code"; 8 | import setupStyles from "./styles"; 9 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; 10 | import { vscDarkPlus } from "../../vscDarkPlus"; 11 | import Link from "next/link"; 12 | 13 | import { themeAtom } from "../Header"; 14 | import { useAtom } from 'jotai'; 15 | 16 | 17 | function Page() { 18 | const [theme] = useAtom(themeAtom) 19 | return ( 20 |
21 |
22 | 34 | {String(children).replace(/\n$/, "")} 35 | 36 | ) : ( 37 | 38 | {children} 39 | 40 | ); 41 | }, 42 | }} 43 | > 44 | {markdown} 45 | 46 |
47 | 48 | {"<-"} Prev 49 | 50 | 51 | Next {"->"} 52 | 53 |
54 |
55 | 56 | 80 |
81 | ); 82 | } 83 | 84 | export default Page; 85 | -------------------------------------------------------------------------------- /app/quick-start/official-utils/styles.ts: -------------------------------------------------------------------------------- 1 | const StylesCss = ` 2 | * { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | body { 7 | font-family: sans-serif; 8 | font-weight: normal; 9 | } 10 | .util { 11 | color: #3ccaad; 12 | position: absolute; 13 | float: inherit; 14 | left: 1%; 15 | top: 2%; 16 | } 17 | .counter { 18 | position: relative; 19 | padding: 10px; 20 | width: 100vw; 21 | height: max-content; 22 | display: flex; 23 | justify-content: center; 24 | flex-direction: column; 25 | gap: 15px; 26 | align-items: center; 27 | } 28 | .counter::after { 29 | content: ''; 30 | display: block; 31 | width: 100vw; 32 | height: 2px; 33 | background: #efefef; 34 | } 35 | .selectAtom { 36 | position: relative; 37 | padding: 10px; 38 | width: 100vw; 39 | height: max-content; 40 | display: flex; 41 | justify-content: center; 42 | flex-direction: column; 43 | gap: 15px; 44 | align-items: center; 45 | } 46 | .btn-grp { 47 | display: flex; 48 | justify-content: center; 49 | gap: 15px; 50 | align-items: center; 51 | } 52 | .btn-grp>button { 53 | font-size: 15px; 54 | width: 120px; 55 | height: 45px; 56 | } 57 | button { 58 | height: 45px; 59 | width: 110px; 60 | border: 2px solid #4f4e4e; 61 | cursor: pointer; 62 | font-size: 17px; 63 | margin-right: 15px; 64 | } 65 | `; 66 | 67 | const setupStyles = { 68 | "/styles.css": { 69 | code: StylesCss, 70 | hidden: true, 71 | }, 72 | }; 73 | 74 | export default setupStyles; 75 | -------------------------------------------------------------------------------- /app/quick-start/persisting-state/code.ts: -------------------------------------------------------------------------------- 1 | const code = `import { useAtom } from 'jotai'; 2 | import { atomWithStorage } from 'jotai/utils'; 3 | 4 | const theme = atomWithStorage('dark', false); 5 | 6 | export default function Page() { 7 | const [appTheme, setAppTheme] = useAtom(theme); 8 | const handleClick = () => setAppTheme(!appTheme); 9 | return ( 10 |
11 |

This is a theme switcher

12 | 13 |
14 | ) 15 | }`; 16 | 17 | const files = { 18 | "/App.js": { 19 | code: code, 20 | }, 21 | }; 22 | 23 | export default files; 24 | -------------------------------------------------------------------------------- /app/quick-start/persisting-state/markdown.ts: -------------------------------------------------------------------------------- 1 | export const markdown = ` 2 | # Persisting state value 3 | 4 | In this lesson, we will take a look at how we can persist the state value to \`localStorage\` with jotai \`atoms\`. 5 | Persisting state to \`localStorage\` can be challenging. You might want to persist the user's preferences or data for their next session. 6 | 7 | Jotai \`atomWithStorage\` is a special kind of atom that automatically syncs the value provided to it with localstorage or sessionStorage, and picks the value upon the first load automatically. It's available in the \`jotai/utils\` module. 8 | To persist our theme atom simply create it with the \`atomWithStorage\` atom. 9 | ~~~js 10 | const theme = atomWithStorage('dark', false) 11 | ~~~ 12 | 13 | Now, when you reload the preview section of the editor you will see that the theme matches the value from before the page reload. 14 | `; 15 | -------------------------------------------------------------------------------- /app/quick-start/persisting-state/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { Sandpack } from "@codesandbox/sandpack-react"; 5 | import Markdown from "react-markdown"; 6 | import { markdown } from "./markdown"; 7 | import files from "./code"; 8 | import setupStyles from "./styles"; 9 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; 10 | import { vscDarkPlus } from "../../vscDarkPlus"; 11 | import Link from "next/link"; 12 | 13 | import { themeAtom } from "../Header"; 14 | import { useAtom } from 'jotai'; 15 | 16 | 17 | function Page() { 18 | const [theme] = useAtom(themeAtom) 19 | return ( 20 |
21 |
22 | 34 | {String(children).replace(/\n$/, "")} 35 | 36 | ) : ( 37 | 38 | {children} 39 | 40 | ); 41 | }, 42 | }} 43 | > 44 | {markdown} 45 | 46 |
47 | 48 | {"<-"} Prev 49 | 50 | 51 | Next {"->"} 52 | 53 |
54 |
55 | 56 | 79 |
80 | ); 81 | } 82 | 83 | export default Page; 84 | -------------------------------------------------------------------------------- /app/quick-start/persisting-state/styles.ts: -------------------------------------------------------------------------------- 1 | const StylesCss = ` 2 | * { 3 | padding: 0; 4 | margin: 0; 5 | } 6 | body { 7 | font-family: sans-serif; 8 | font-weight: normal; 9 | } 10 | .dark { 11 | color: white; 12 | background: black; 13 | width: 100vw; 14 | height: 100vh; 15 | display: flex; 16 | justify-content: center; 17 | flex-direction: column; 18 | gap: 20px; 19 | align-items: center; 20 | } 21 | .light { 22 | color: black; 23 | background: white; 24 | width: 100vw; 25 | height: 100vh; 26 | display: flex; 27 | flex-direction: column; 28 | justify-content: center; 29 | gap: 20px; 30 | align-items: center; 31 | } 32 | button { 33 | // margin-left: -40px; 34 | height: 45px; 35 | width: 100px; 36 | border: 2px solid #4f4e4e; 37 | cursor: pointer; 38 | font-size: 17px; 39 | } 40 | `; 41 | 42 | const setupStyles = { 43 | "/styles.css": { 44 | code: StylesCss, 45 | hidden: true, 46 | }, 47 | }; 48 | 49 | export default setupStyles; 50 | -------------------------------------------------------------------------------- /app/quick-start/read-write-atoms/code.ts: -------------------------------------------------------------------------------- 1 | const code = `import { atom, useAtom } from "jotai"; 2 | 3 | const dotsAtom = atom([]); 4 | 5 | const drawingAtom = atom(false); 6 | 7 | const handleMouseDownAtom = atom( 8 | null, 9 | (get, set) => { 10 | set(drawingAtom, true); 11 | } 12 | ); 13 | 14 | const handleMouseUpAtom = atom(null, (get, set) => { 15 | set(drawingAtom, false); 16 | }); 17 | 18 | const handleMouseMoveAtom = atom( 19 | (get) => get(dotsAtom), 20 | (get, set, update: Point) => { 21 | if (get(drawingAtom)) { 22 | set(dotsAtom, (prev) => [...prev, update]); 23 | } 24 | } 25 | ); 26 | 27 | const SvgDots = () => { 28 | const [dots] = useAtom(handleMouseMoveAtom); 29 | return ( 30 | 31 | {dots.map(([x, y], index) => ( 32 | 33 | ))} 34 | 35 | ); 36 | }; 37 | 38 | const SvgRoot = () => { 39 | const [, handleMouseUp] = useAtom( 40 | handleMouseUpAtom 41 | ); 42 | const [, handleMouseDown] = useAtom( 43 | handleMouseDownAtom 44 | ); 45 | const [, handleMouseMove] = useAtom( 46 | handleMouseMoveAtom 47 | ); 48 | return ( 49 | { 56 | handleMouseMove([e.clientX, e.clientY]); 57 | }} 58 | > 59 | 60 | 61 | 62 | ); 63 | }; 64 | 65 | const App = () => ( 66 | <> 67 | 68 | 69 | ); 70 | 71 | export default App; 72 | 73 | `; 74 | 75 | const files = { 76 | "/App.js": { 77 | code: code, 78 | }, 79 | }; 80 | 81 | export default files; 82 | -------------------------------------------------------------------------------- /app/quick-start/read-write-atoms/markdown.ts: -------------------------------------------------------------------------------- 1 | export const markdown = ` 2 | # Read Write atoms 3 | 4 | These atoms are the combination of both read-only and write-only atoms. 5 | 6 | ~~~js 7 | const count = atom(1); 8 | export const readWriteAtom = atom((get) => get(count), 9 | (get, set) => { 10 | set(count, get(count) + 1); 11 | }, 12 | ); 13 | ~~~ 14 | 15 | The first parameter is for reading and the second is for modifying the atom value. 16 | Since the \`readWriteAtom\` is capable to read and set the original atom value, so we can only export \`readWriteAtom\` atom and can hide the original atom in a smaller scope. In this way we have to deal with less number of atoms in our app. 17 | 18 | See the code how we use only \`handleMouseMoveAtom\` to read and update both the \`dotsArray\` in our app. 19 | ` -------------------------------------------------------------------------------- /app/quick-start/read-write-atoms/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { Sandpack } from "@codesandbox/sandpack-react"; 5 | import Markdown from "react-markdown"; 6 | import { markdown } from "./markdown"; 7 | import files from "./code"; 8 | import setupStyles from "./styles"; 9 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; 10 | import { vscDarkPlus } from "../../vscDarkPlus"; 11 | import Link from "next/link"; 12 | 13 | import { themeAtom } from "../Header"; 14 | import { useAtom } from 'jotai'; 15 | 16 | 17 | function Page() { 18 | const [theme] = useAtom(themeAtom) 19 | return ( 20 |
21 |
22 | 34 | {String(children).replace(/\n$/, "")} 35 | 36 | ) : ( 37 | 38 | {children} 39 | 40 | ); 41 | }, 42 | }} 43 | > 44 | {markdown} 45 | 46 |
47 | 48 | {"<-"} Prev 49 | 50 | 51 | Next {"->"} 52 | 53 |
54 |
55 | 56 | 79 |
80 | ); 81 | } 82 | 83 | export default Page; 84 | -------------------------------------------------------------------------------- /app/quick-start/read-write-atoms/styles.ts: -------------------------------------------------------------------------------- 1 | const StylesCss = ` 2 | * { 3 | padding: 0; 4 | margin: 0; 5 | } 6 | .app { 7 | width: 100vw; 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: center; 11 | align-items: center; 12 | } 13 | button { 14 | // margin-left: -40px; 15 | height: 45px; 16 | width: 100px; 17 | border: 2px solid #4f4e4e; 18 | cursor: pointer; 19 | font-size: 17px; 20 | } 21 | `; 22 | 23 | const setupStyles = { 24 | "/styles.css": { 25 | code: StylesCss, 26 | hidden: true, 27 | }, 28 | }; 29 | 30 | export default setupStyles; 31 | -------------------------------------------------------------------------------- /app/quick-start/readonly-atoms/code.ts: -------------------------------------------------------------------------------- 1 | const code = `import { atom, useAtom } from 'jotai'; 2 | 3 | const textAtom = atom('readonly atoms') 4 | const uppercase = atom((get) => get(textAtom).toUpperCase()) 5 | 6 | export default function Page() { 7 | const [lowercaseText, setLowercaseText] = useAtom(textAtom); 8 | const [uppercaseText] = useAtom(uppercase); 9 | const handleChange = (e) => setLowercaseText(e.target.value); 10 | return ( 11 |
12 | 13 |

{uppercaseText}

14 |
15 | ) 16 | }`; 17 | 18 | const files = { 19 | "/App.js": { 20 | code: code, 21 | }, 22 | }; 23 | 24 | export default files; 25 | -------------------------------------------------------------------------------- /app/quick-start/readonly-atoms/markdown.ts: -------------------------------------------------------------------------------- 1 | export const markdown = ` 2 | # Read Only atoms 3 | 4 | Readonly atoms are used to read the value of the other atoms. You can't set or change their value directly because these atoms rely on their parent atoms. 5 | 6 | ~~~js 7 | const textAtom = atom('readonly'); 8 | const uppercase = atom((get) => get(textAtom).toUpperCase()); 9 | ~~~ 10 | 11 | These atoms takes a callback with a parameter \`get\` which allows us to read other atoms value. Changing the parent's value will also update the derived atom. 12 | 13 | ~~~js 14 | const firstName = atom('John'); 15 | const lastName = atom('Harris'); 16 | const fullName = atom((get) => get(firstName) + " " + get(lastName)); 17 | ~~~ 18 | 19 | You can do more than just simply read the value of other atoms like \`filter\` and \`sorted\` out them or \`map\` over the values of the parent atom. And this is the beauty of it, Jotai gracefully lets you create dumb atoms derivated from even more dumb atoms. 20 | Here is a example of getting the list of all online and offline friends. 21 | ~~~js 22 | const friendsStatus = atom([ 23 | { name: "John", online: true }, 24 | { name: "David", online: false }, 25 | { name: "Micheal", online: true } 26 | ]); 27 | 28 | const onlineFriends = atom((get) => get(friendsStatus).filter((item) => item.online)); 29 | const offlineFriends = atom((get) => get(friendsStatus).filter((item) => !item.online)); 30 | ~~~ 31 | ` 32 | -------------------------------------------------------------------------------- /app/quick-start/readonly-atoms/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { Sandpack } from "@codesandbox/sandpack-react"; 5 | import Markdown from "react-markdown"; 6 | import { markdown } from "./markdown"; 7 | import files from "./code"; 8 | import setupStyles from "./styles"; 9 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; 10 | import { vscDarkPlus } from "../../vscDarkPlus"; 11 | import Link from "next/link"; 12 | 13 | import { themeAtom } from "../Header"; 14 | import { useAtom } from 'jotai'; 15 | 16 | 17 | function Page() { 18 | const [theme] = useAtom(themeAtom) 19 | return ( 20 |
21 |
22 | 34 | {String(children).replace(/\n$/, "")} 35 | 36 | ) : ( 37 | 38 | {children} 39 | 40 | ); 41 | }, 42 | }} 43 | > 44 | {markdown} 45 | 46 |
47 | 48 | {"<-"} Prev 49 | 50 | 51 | Next {"->"} 52 | 53 |
54 |
55 | 56 | 79 |
80 | ); 81 | } 82 | 83 | export default Page; 84 | -------------------------------------------------------------------------------- /app/quick-start/readonly-atoms/styles.ts: -------------------------------------------------------------------------------- 1 | const StylesCss = ` 2 | body { 3 | font-family: sans-serif; 4 | } 5 | .app { 6 | width: 100vw; 7 | height: 100vh; 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: center; 11 | align-items: center; 12 | } 13 | input { 14 | height: 30px; 15 | font-size: 17px; 16 | margin-bottom: -10px; 17 | } 18 | `; 19 | 20 | const setupStyles = { 21 | "/styles.css": { 22 | code: StylesCss, 23 | hidden: true, 24 | }, 25 | }; 26 | 27 | export default setupStyles; 28 | -------------------------------------------------------------------------------- /app/quick-start/theme-setting/code.ts: -------------------------------------------------------------------------------- 1 | const code = `import { atom, useAtom } from 'jotai'; 2 | 3 | const theme = atom('light'); 4 | 5 | export default function Page() { 6 | const [appTheme, setAppTheme] = useAtom(theme); 7 | const handleClick = () => setAppTheme(appTheme === 'light'? 'dark': 'light'); 8 | return ( 9 |
10 |

This is a theme switcher

11 | 12 |
13 | ) 14 | }`; 15 | 16 | const files = { 17 | "/App.js": { 18 | code: code, 19 | }, 20 | }; 21 | 22 | export default files; 23 | -------------------------------------------------------------------------------- /app/quick-start/theme-setting/markdown.ts: -------------------------------------------------------------------------------- 1 | export const markdown = ` 2 | # Theme Switcher 3 | 4 | Developers love dark theme but setting up the theme can be very hectic for the developers as when there are many components in your app and you have to pass your theme props very deep in the component tree where things can become ambigous. 5 | 6 | With the help of jotai, you can setup different themes for your app in minutes. Let's take a look at this: 7 | 8 | Initialize a theme atom with a default value. 9 | 10 | ~~~js 11 | const theme = atom('light'); 12 | ~~~ 13 | 14 | Passed the atom to the \`useState\` hook. 15 | 16 | ~~~js 17 | const [appTheme, setAppTheme] = useAtom(theme); 18 | ~~~ 19 | 20 | Yes, That's all we have to do to define a global theme state which is accessible to all components of your app. 21 | `; 22 | -------------------------------------------------------------------------------- /app/quick-start/theme-setting/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { Sandpack } from "@codesandbox/sandpack-react"; 5 | import Markdown from "react-markdown"; 6 | import { markdown } from "./markdown"; 7 | import files from "./code"; 8 | import setupStyles from "./styles"; 9 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; 10 | import { vscDarkPlus } from "../../vscDarkPlus"; 11 | import Link from "next/link"; 12 | 13 | import { themeAtom } from "../Header"; 14 | import { useAtom } from 'jotai'; 15 | 16 | 17 | function Page() { 18 | const [theme] = useAtom(themeAtom) 19 | return ( 20 |
21 |
22 | 34 | {String(children).replace(/\n$/, "")} 35 | 36 | ) : ( 37 | 38 | {children} 39 | 40 | ); 41 | }, 42 | }} 43 | > 44 | {markdown} 45 | 46 |
47 | 48 | {"<-"} Prev 49 | 50 | 51 | Next {"->"} 52 | 53 |
54 |
55 | 56 | 79 |
80 | ); 81 | } 82 | 83 | export default Page; 84 | -------------------------------------------------------------------------------- /app/quick-start/theme-setting/styles.ts: -------------------------------------------------------------------------------- 1 | const StylesCss = ` 2 | * { 3 | padding: 0; 4 | margin: 0; 5 | } 6 | body { 7 | font-family: sans-serif; 8 | font-weight: normal; 9 | } 10 | .dark { 11 | color: white; 12 | background: black; 13 | width: 100vw; 14 | height: 100vh; 15 | display: flex; 16 | justify-content: center; 17 | flex-direction: column; 18 | gap: 20px; 19 | align-items: center; 20 | } 21 | .light { 22 | color: black; 23 | background: white; 24 | width: 100vw; 25 | height: 100vh; 26 | display: flex; 27 | flex-direction: column; 28 | justify-content: center; 29 | gap: 20px; 30 | align-items: center; 31 | } 32 | button { 33 | // margin-left: -40px; 34 | height: 45px; 35 | width: 100px; 36 | border: 2px solid #4f4e4e; 37 | cursor: pointer; 38 | font-size: 17px; 39 | } 40 | `; 41 | 42 | const setupStyles = { 43 | "/styles.css": { 44 | code: StylesCss, 45 | hidden: true, 46 | }, 47 | }; 48 | 49 | export default setupStyles; 50 | -------------------------------------------------------------------------------- /app/quick-start/write-only-atoms/code.ts: -------------------------------------------------------------------------------- 1 | const code = `import { atom, useAtom } from "jotai"; 2 | 3 | const dotsAtom = atom([]); 4 | 5 | const drawingAtom = atom(false); 6 | 7 | const handleMouseDownAtom = atom( 8 | null, 9 | (get, set) => { 10 | set(drawingAtom, true); 11 | } 12 | ); 13 | 14 | const handleMouseUpAtom = atom(null, (get, set) => { 15 | set(drawingAtom, false); 16 | }); 17 | 18 | const handleMouseMoveAtom = atom( 19 | null, 20 | (get, set, update: Point) => { 21 | if (get(drawingAtom)) { 22 | set(dotsAtom, (prev) => [...prev, update]); 23 | } 24 | } 25 | ); 26 | 27 | const SvgDots = () => { 28 | const [dots] = useAtom(dotsAtom); 29 | return ( 30 | 31 | {dots.map(([x, y], index) => ( 32 | 33 | ))} 34 | 35 | ); 36 | }; 37 | 38 | const SvgRoot = () => { 39 | const [, handleMouseUp] = useAtom( 40 | handleMouseUpAtom 41 | ); 42 | const [, handleMouseDown] = useAtom( 43 | handleMouseDownAtom 44 | ); 45 | const [, handleMouseMove] = useAtom( 46 | handleMouseMoveAtom 47 | ); 48 | return ( 49 | { 56 | handleMouseMove([e.clientX, e.clientY]); 57 | }} 58 | > 59 | 60 | 61 | 62 | ); 63 | }; 64 | 65 | const App = () => ( 66 | <> 67 | 68 | 69 | ); 70 | 71 | export default App; 72 | 73 | `; 74 | 75 | const files = { 76 | "/App.js": { 77 | code: code, 78 | }, 79 | }; 80 | 81 | export default files; 82 | -------------------------------------------------------------------------------- /app/quick-start/write-only-atoms/markdown.ts: -------------------------------------------------------------------------------- 1 | export const markdown = ` 2 | # Write Only atoms 3 | 4 | With the help of writeOnly atoms you can modify the atoms it relies on. It's basically a two-way data binding, changing the derived atom also changes the parent atom and vice-versa, so use these atoms very carefully. 5 | 6 | ~~~js 7 | const textAtom = atom('write only atoms') 8 | const uppercase = atom(null, (get, set) => { 9 | set(textAtom, get(textAtom).toUpperCase()) 10 | }) 11 | ~~~ 12 | 13 | The first value of the callback is always be null and second is the function to modify the atom value. Let's take a more practical use case of write-only atoms. 14 | 15 | Here we define a \`dotsAtom\` which is an atom of positions of points we draw on the canvas and a \`drawing\` atom. 16 | ~~~js 17 | const dotsAtom = atom([]); 18 | // true when we drawing on canvas 19 | const drawingAtom = atom(false); 20 | ~~~ 21 | 22 | The \`handleMouseDownAtom\` and \`handleMouseUpAtom\` are two write-only atom that we use to set the value of \`drawing\` atom and \`handleMouseMoveAtom\` is a write-only atom which adds the position of new points to the \`dotsArray\` atom when we drawing on the canvas. 23 | 24 | ~~~js 25 | const handleMouseMoveAtom = atom( 26 | null, 27 | (get, set, update: Point) => { 28 | if (get(drawingAtom)) { 29 | set(dotsAtom, (prev) => [...prev, update]); 30 | } 31 | } 32 | ); 33 | ~~~ 34 | 35 | **Note:** You must be thinking that why we not updating the atoms value directly, why we use a write-only atom to update it's value. Well updating the value using the write-only atom prevents the extra rerenders in our app. 36 | ` -------------------------------------------------------------------------------- /app/quick-start/write-only-atoms/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { Sandpack } from "@codesandbox/sandpack-react"; 5 | import Markdown from "react-markdown"; 6 | import { markdown } from "./markdown"; 7 | import files from "./code"; 8 | import setupStyles from "./styles"; 9 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; 10 | import { vscDarkPlus } from "../../vscDarkPlus"; 11 | import Link from "next/link"; 12 | 13 | import { themeAtom } from "../Header"; 14 | import { useAtom } from 'jotai'; 15 | 16 | 17 | function Page() { 18 | const [theme] = useAtom(themeAtom) 19 | return ( 20 |
21 |
22 | 34 | {String(children).replace(/\n$/, "")} 35 | 36 | ) : ( 37 | 38 | {children} 39 | 40 | ); 41 | }, 42 | }} 43 | > 44 | {markdown} 45 | 46 |
47 | 48 | {"<-"} Prev 49 | 50 | 51 | Next {"->"} 52 | 53 |
54 |
55 | 56 | 79 |
80 | ); 81 | } 82 | 83 | export default Page; 84 | -------------------------------------------------------------------------------- /app/quick-start/write-only-atoms/styles.ts: -------------------------------------------------------------------------------- 1 | const StylesCss = ` 2 | * { 3 | padding: 0; 4 | margin: 0; 5 | } 6 | body { 7 | font-family: sans-serif; 8 | } 9 | `; 10 | 11 | const setupStyles = { 12 | "/styles.css": { 13 | code: StylesCss, 14 | hidden: true, 15 | }, 16 | }; 17 | 18 | export default setupStyles; 19 | -------------------------------------------------------------------------------- /app/quick-start/write-only-atoms/write-only-atom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jotaijs/jotai-tutorial/010a9f8bbf99873dbdea319d2980b1602e1a68a9/app/quick-start/write-only-atoms/write-only-atom.png -------------------------------------------------------------------------------- /app/vscDarkPlus.js: -------------------------------------------------------------------------------- 1 | export const vscDarkPlus = { 2 | "pre[class*=\"language-\"]": { 3 | "color": "#e4f0fc", 4 | "fontSize": "13px", 5 | "textShadow": "none", 6 | "fontFamily": "Menlo, Monaco, Consolas, \"Andale Mono\", \"Ubuntu Mono\", \"Courier New\", monospace", 7 | "direction": "ltr", 8 | "textAlign": "left", 9 | "whiteSpace": "pre", 10 | "wordSpacing": "normal", 11 | "wordBreak": "normal", 12 | "lineHeight": "1.5", 13 | "MozTabSize": "4", 14 | "OTabSize": "4", 15 | "tabSize": "4", 16 | "WebkitHyphens": "none", 17 | "MozHyphens": "none", 18 | "msHyphens": "none", 19 | "hyphens": "none", 20 | "padding": "1em", 21 | "margin": ".5em 0", 22 | "overflow": "auto", 23 | "background": "#1e1e1e" 24 | }, 25 | "code[class*=\"language-\"]": { 26 | "color": "#e4f0fc", 27 | "fontSize": "13px", 28 | "textShadow": "none", 29 | "fontFamily": "Menlo, Monaco, Consolas, \"Andale Mono\", \"Ubuntu Mono\", \"Courier New\", monospace", 30 | "direction": "ltr", 31 | "textAlign": "left", 32 | "whiteSpace": "pre", 33 | "wordSpacing": "normal", 34 | "wordBreak": "normal", 35 | "lineHeight": "1.5", 36 | "MozTabSize": "4", 37 | "OTabSize": "4", 38 | "tabSize": "4", 39 | "WebkitHyphens": "none", 40 | "MozHyphens": "none", 41 | "msHyphens": "none", 42 | "hyphens": "none" 43 | }, 44 | "pre[class*=\"language-\"]::selection": { 45 | "textShadow": "none", 46 | "background": "#264F78" 47 | }, 48 | "code[class*=\"language-\"]::selection": { 49 | "textShadow": "none", 50 | "background": "#264F78" 51 | }, 52 | "pre[class*=\"language-\"] *::selection": { 53 | "textShadow": "none", 54 | "background": "#264F78" 55 | }, 56 | "code[class*=\"language-\"] *::selection": { 57 | "textShadow": "none", 58 | "background": "#264F78" 59 | }, 60 | ":not(pre) > code[class*=\"language-\"]": { 61 | "padding": ".1em .3em", 62 | "borderRadius": ".3em", 63 | "color": "#db4c69", 64 | "background": "#1e1e1e" 65 | }, 66 | ".namespace": { 67 | "Opacity": ".7" 68 | }, 69 | "doctype.doctype-tag": { 70 | "color": "#acd7ff" 71 | }, 72 | "doctype.name": { 73 | "color": "#9cdcfe" 74 | }, 75 | "comment": { 76 | "color": "#a6accd" 77 | }, 78 | "prolog": { 79 | "color": "#6a9955" 80 | }, 81 | "punctuation": { 82 | "color": "#e4f0fc" 83 | }, 84 | ".language-html .language-css .token.punctuation": { 85 | "color": "#e4f0fc" 86 | }, 87 | ".language-html .language-javascript .token.punctuation": { 88 | "color": "#e4f0fc" 89 | }, 90 | "property": { 91 | "color": "#9cdcfe" 92 | }, 93 | "tag": { 94 | "color": "#569cd6" 95 | }, 96 | "boolean": { 97 | "color": "#569cd6" 98 | }, 99 | "number": { 100 | "color": "#e4f0fc" 101 | }, 102 | "constant": { 103 | "color": "#9cdcfe" 104 | }, 105 | "symbol": { 106 | "color": "#e4f0fc" 107 | }, 108 | "inserted": { 109 | "color": "#e4f0fc" 110 | }, 111 | "unit": { 112 | "color": "#e4f0fc" 113 | }, 114 | "selector": { 115 | "color": "#d7ba7d" 116 | }, 117 | "attr-name": { 118 | "color": "#9cdcfe" 119 | }, 120 | "string": { 121 | "color": "#5de4c7" 122 | }, 123 | "char": { 124 | "color": "#5de4c7" 125 | }, 126 | "builtin": { 127 | "color": "#5de4c7" 128 | }, 129 | "deleted": { 130 | "color": "#5de4c7" 131 | }, 132 | ".language-css .token.string.url": { 133 | "textDecoration": "underline" 134 | }, 135 | "operator": { 136 | "color": "#e4f0fc" 137 | }, 138 | "entity": { 139 | "color": "#569cd6" 140 | }, 141 | "operator.arrow": { 142 | "color": "#acd7ff" 143 | }, 144 | "atrule": { 145 | "color": "#5de4c7" 146 | }, 147 | "atrule.rule": { 148 | "color": "#e4f0fc" 149 | }, 150 | "atrule.url": { 151 | "color": "#9cdcfe" 152 | }, 153 | "atrule.url.function": { 154 | "color": "#5de4c7" 155 | }, 156 | "atrule.url.punctuation": { 157 | "color": "#e4f0fc" 158 | }, 159 | "keyword": { 160 | "color": "#acd7ff" 161 | }, 162 | "keyword.module": { 163 | "color": "#acd7ff" 164 | }, 165 | "keyword.control-flow": { 166 | "color": "#e4f0fc" 167 | }, 168 | "function": { 169 | "color": "#5de4c7" 170 | }, 171 | "function.maybe-class-name": { 172 | "color": "#5de4c7" 173 | }, 174 | "regex": { 175 | "color": "#d16969" 176 | }, 177 | "important": { 178 | "color": "#569cd6" 179 | }, 180 | "italic": { 181 | "fontStyle": "italic" 182 | }, 183 | "class-name": { 184 | "color": "#4ec9b0" 185 | }, 186 | "maybe-class-name": { 187 | "color": "#4ec9b0" 188 | }, 189 | "console": { 190 | "color": "#9cdcfe" 191 | }, 192 | "parameter": { 193 | "color": "#9cdcfe" 194 | }, 195 | "interpolation": { 196 | "color": "#9cdcfe" 197 | }, 198 | "punctuation.interpolation-punctuation": { 199 | "color": "#569cd6" 200 | }, 201 | "variable": { 202 | "color": "#9cdcfe" 203 | }, 204 | "imports.maybe-class-name": { 205 | "color": "#9cdcfe" 206 | }, 207 | "exports.maybe-class-name": { 208 | "color": "#9cdcfe" 209 | }, 210 | "escape": { 211 | "color": "#d7ba7d" 212 | }, 213 | "tag.punctuation": { 214 | "color": "#808080" 215 | }, 216 | "cdata": { 217 | "color": "#808080" 218 | }, 219 | "attr-value": { 220 | "color": "#5de4c7" 221 | }, 222 | "attr-value.punctuation": { 223 | "color": "#5de4c7" 224 | }, 225 | "attr-value.punctuation.attr-equals": { 226 | "color": "#e4f0fc" 227 | }, 228 | "namespace": { 229 | "color": "#4ec9b0" 230 | }, 231 | "pre[class*=\"language-javascript\"]": { 232 | "color": "#9cdcfe" 233 | }, 234 | "code[class*=\"language-javascript\"]": { 235 | "color": "#9cdcfe" 236 | }, 237 | "pre[class*=\"language-jsx\"]": { 238 | "color": "#9cdcfe" 239 | }, 240 | "code[class*=\"language-jsx\"]": { 241 | "color": "#9cdcfe" 242 | }, 243 | "pre[class*=\"language-typescript\"]": { 244 | "color": "#9cdcfe" 245 | }, 246 | "code[class*=\"language-typescript\"]": { 247 | "color": "#9cdcfe" 248 | }, 249 | "pre[class*=\"language-tsx\"]": { 250 | "color": "#9cdcfe" 251 | }, 252 | "code[class*=\"language-tsx\"]": { 253 | "color": "#9cdcfe" 254 | }, 255 | "pre[class*=\"language-css\"]": { 256 | "color": "#5de4c7" 257 | }, 258 | "code[class*=\"language-css\"]": { 259 | "color": "#5de4c7" 260 | }, 261 | "pre[class*=\"language-html\"]": { 262 | "color": "#e4f0fc" 263 | }, 264 | "code[class*=\"language-html\"]": { 265 | "color": "#e4f0fc" 266 | }, 267 | ".language-regex .token.anchor": { 268 | "color": "#5de4c7" 269 | }, 270 | ".language-html .token.punctuation": { 271 | "color": "#808080" 272 | }, 273 | "pre[class*=\"language-\"] > code[class*=\"language-\"]": { 274 | "position": "relative", 275 | "zIndex": "1" 276 | }, 277 | ".line-highlight.line-highlight": { 278 | "background": "#f7ebc6", 279 | "boxShadow": "inset 5px 0 0 #f7d87c", 280 | "zIndex": "0" 281 | } 282 | } -------------------------------------------------------------------------------- /declaration.d.ts: -------------------------------------------------------------------------------- 1 | import { AriaAttributes, DOMAttributes } from "react"; 2 | 3 | declare module "react" { 4 | interface HTMLAttributes extends AriaAttributes, DOMAttributes { 5 | rel?: string; 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | appDir: true, 5 | }, 6 | }; 7 | 8 | module.exports = nextConfig; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jotai-tutorial", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@codesandbox/sandpack-client": "^2.0.1", 13 | "@codesandbox/sandpack-react": "^2.0.6", 14 | "@next/font": "^13.1.6", 15 | "@types/node": "18.14.0", 16 | "@types/react": "18.0.28", 17 | "@types/react-dom": "18.0.11", 18 | "eslint": "8.34.0", 19 | "eslint-config-next": "13.1.6", 20 | "jotai": "^2.0.2", 21 | "jotai-devtools": "^0.2.0", 22 | "next": "13.1.6", 23 | "react": "18.2.0", 24 | "react-dom": "18.2.0", 25 | "react-icons": "^4.7.1", 26 | "react-markdown": "^8.0.5", 27 | "react-syntax-highlighter": "^15.5.0", 28 | "typescript": "4.9.5" 29 | }, 30 | "devDependencies": { 31 | "@types/react-syntax-highlighter": "^15.5.6", 32 | "prettier": "2.8.4" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jotaijs/jotai-tutorial/010a9f8bbf99873dbdea319d2980b1602e1a68a9/public/github.png -------------------------------------------------------------------------------- /public/jotai-mascot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jotaijs/jotai-tutorial/010a9f8bbf99873dbdea319d2980b1602e1a68a9/public/jotai-mascot.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ] 22 | }, 23 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 24 | "exclude": ["node_modules"] 25 | } 26 | --------------------------------------------------------------------------------