{"id":13550599,"url":"https://github.com/grammarly/focal","last_synced_at":"2026-02-21T13:20:59.641Z","repository":{"id":18562205,"uuid":"84226707","full_name":"grammarly/focal","owner":"grammarly","description":"Program user interfaces the FRP way.","archived":false,"fork":false,"pushed_at":"2023-10-23T16:55:35.000Z","size":1368,"stargazers_count":729,"open_issues_count":13,"forks_count":39,"subscribers_count":27,"default_branch":"master","last_synced_at":"2025-09-22T10:49:20.117Z","etag":null,"topics":["lenses","react","rxjs","state-management","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/grammarly.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2017-03-07T17:21:41.000Z","updated_at":"2025-07-10T10:35:17.000Z","dependencies_parsed_at":"2023-10-14T18:06:57.105Z","dependency_job_id":"d459e3a6-678b-4099-a5bc-d9d3a7f87784","html_url":"https://github.com/grammarly/focal","commit_stats":{"total_commits":360,"total_committers":14,"mean_commits":"25.714285714285715","dds":0.475,"last_synced_commit":"df10b5a5afb10dde484e34728777ae82db7225fc"},"previous_names":[],"tags_count":24,"template":false,"template_full_name":null,"purl":"pkg:github/grammarly/focal","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grammarly%2Ffocal","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grammarly%2Ffocal/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grammarly%2Ffocal/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grammarly%2Ffocal/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/grammarly","download_url":"https://codeload.github.com/grammarly/focal/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grammarly%2Ffocal/sbom","scorecard":{"id":443423,"data":{"date":"2025-08-11","repo":{"name":"github.com/grammarly/focal","commit":"745d889528aa8d6ab04be62fc18975d082db7e17"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3.3,"checks":[{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/tests.yml:1","Warn: no topLevel permission defined: .github/workflows/version-and-release.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Code-Review","score":9,"reason":"Found 11/12 approved changesets -- score normalized to 9","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/tests.yml:11: update your workflow using https://app.stepsecurity.io/secureworkflow/grammarly/focal/tests.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/tests.yml:12: update your workflow using https://app.stepsecurity.io/secureworkflow/grammarly/focal/tests.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/version-and-release.yml:19: update your workflow using https://app.stepsecurity.io/secureworkflow/grammarly/focal/version-and-release.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/version-and-release.yml:20: update your workflow using https://app.stepsecurity.io/secureworkflow/grammarly/focal/version-and-release.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/version-and-release.yml:27: update your workflow using https://app.stepsecurity.io/secureworkflow/grammarly/focal/version-and-release.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/version-and-release.yml:37: update your workflow using https://app.stepsecurity.io/secureworkflow/grammarly/focal/version-and-release.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/version-and-release.yml:54: update your workflow using https://app.stepsecurity.io/secureworkflow/grammarly/focal/version-and-release.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/version-and-release.yml:55: update your workflow using https://app.stepsecurity.io/secureworkflow/grammarly/focal/version-and-release.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/version-and-release.yml:68: update your workflow using https://app.stepsecurity.io/secureworkflow/grammarly/focal/version-and-release.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/version-and-release.yml:76: update your workflow using https://app.stepsecurity.io/secureworkflow/grammarly/focal/version-and-release.yml/master?enable=pin","Info:   0 out of   6 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   4 third-party GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: Apache License 2.0: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":0,"reason":"Project has not signed or included provenance with any releases.","details":["Warn: release artifact @grammarly/focal@0.11.0 not signed: https://api.github.com/repos/grammarly/focal/releases/126287119","Warn: release artifact @grammarly/focal-atom@0.11.0 not signed: https://api.github.com/repos/grammarly/focal/releases/126287128","Warn: release artifact v0.10.2 not signed: https://api.github.com/repos/grammarly/focal/releases/125329956","Warn: release artifact v0.10.1 not signed: https://api.github.com/repos/grammarly/focal/releases/125038602","Warn: release artifact v0.10.0-alpha.0 not signed: https://api.github.com/repos/grammarly/focal/releases/98366332","Warn: release artifact @grammarly/focal@0.11.0 does not have provenance: https://api.github.com/repos/grammarly/focal/releases/126287119","Warn: release artifact @grammarly/focal-atom@0.11.0 does not have provenance: https://api.github.com/repos/grammarly/focal/releases/126287128","Warn: release artifact v0.10.2 does not have provenance: https://api.github.com/repos/grammarly/focal/releases/125329956","Warn: release artifact v0.10.1 does not have provenance: https://api.github.com/repos/grammarly/focal/releases/125038602","Warn: release artifact v0.10.0-alpha.0 does not have provenance: https://api.github.com/repos/grammarly/focal/releases/98366332"],"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 29 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":0,"reason":"21 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-968p-4wvh-cqc8","Warn: Project is vulnerable to: GHSA-67hx-6x53-jw92","Warn: Project is vulnerable to: GHSA-qwcr-r2fm-qrc7","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg","Warn: Project is vulnerable to: GHSA-pxg6-pf52-xh8x","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-rv95-896h-c2vc","Warn: Project is vulnerable to: GHSA-qw6h-vgh9-j6wx","Warn: Project is vulnerable to: GHSA-fjxv-7rqg-78g4","Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv","Warn: Project is vulnerable to: GHSA-9wv6-86v2-598j","Warn: Project is vulnerable to: GHSA-rhx6-c78j-4q9w","Warn: Project is vulnerable to: GHSA-7fh5-64p2-3v2j","Warn: Project is vulnerable to: GHSA-m6fv-jmcg-4jfg","Warn: Project is vulnerable to: GHSA-76p7-773f-r4q5","Warn: Project is vulnerable to: GHSA-cm22-4g7w-348p","Warn: Project is vulnerable to: GHSA-52f5-9888-hmc6","Warn: Project is vulnerable to: GHSA-4vvj-4cpr-p986","Warn: Project is vulnerable to: GHSA-wr3j-pwj9-hqq6","Warn: Project is vulnerable to: GHSA-3h5v-q93c-6h6q"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-19T06:07:02.914Z","repository_id":18562205,"created_at":"2025-08-19T06:07:02.914Z","updated_at":"2025-08-19T06:07:02.914Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29681528,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-21T12:30:22.644Z","status":"ssl_error","status_checked_at":"2026-02-21T12:29:55.402Z","response_time":107,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["lenses","react","rxjs","state-management","typescript"],"created_at":"2024-08-01T12:01:35.192Z","updated_at":"2026-02-21T13:20:59.608Z","avatar_url":"https://github.com/grammarly.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# \u003ca href=\"http://github.com/grammarly/focal\"\u003e\u003cimg src=\"https://raw.githubusercontent.com/grammarly/focal/master/logo/logo.png\" alt=\"Focal 🔍\" height=\"100\"\u003e\u003c/a\u003e\n\n[![Build Status](https://travis-ci.org/grammarly/focal.svg?branch=master)](https://travis-ci.org/grammarly/focal) [![npm version](https://img.shields.io/npm/v/@grammarly/focal.svg)](https://www.npmjs.com/package/@grammarly/focal) [![Apache 2.0 license](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/grammarly/focal#license)\n\nType safe, expressive and composable state management for [React](https://facebook.github.io/react/) applications.\n\n- Represent the whole application state as an immutable and [observable](http://reactivex.io/) single source of truth.\n- Seamlessly embed observables into React components' layout.\n- Leverage the power of [Rx.JS](https://rxjs.dev) (and observables in general) to enrich and combine parts of application state, explicitly controlling the data flow.\n- Use [lenses](https://en.wikibooks.org/wiki/Haskell/Lenses_and_functional_references) to decompose the application state into smaller parts, so you can isolate UI components in a clean way and manipulate application state effortlessly.\n- Write less code that is easier to understand.\n\n# Packages\n\n`@grammarly/focal` - Type safe, expressive and composable state management for [React](https://facebook.github.io/react/) applications.\n`@grammarly/focal-atom` - Type safe, expressive and composable state management for any application.\n\n# Example\n\nHere's a typical example of a 'counter' UI component and how it fits within the whole application:\n\n```typescript\nimport * as React from 'react'\nimport * as ReactDOM from 'react-dom'\nimport {\n  Atom,\n  // this is the special namespace with React components that accept\n  // observable values in their props\n  F\n} from '@grammarly/focal'\n\n// our counter UI component\nconst Counter = (props: { count: Atom\u003cnumber\u003e }) =\u003e\n  \u003cF.div\u003e\n    {/* use observable state directly in JSX */}\n    You have clicked this button {props.count} time(s).\n\n    \u003cbutton\n      onClick={() =\u003e\n        // update the counter state on click\n        props.count.modify(x =\u003e x + 1)\n      }\n    \u003e\n      Click again?\n    \u003c/button\u003e\n  \u003c/F.div\u003e\n\n// the main 'app' UI component\nconst App = (props: { state: Atom\u003c{ count: number }\u003e }) =\u003e\n  \u003cdiv\u003e\n    Hello, world!\n    \u003cCounter\n      count={\n        // take the app state and lens into its part where the\n        // counter's state lies.\n        //\n        // this creates an atom which you can write to, in a type safe way.\n        props.state.lens('count')\n      }\n    /\u003e\n  \u003c/div\u003e\n\n// create the app state atom\nconst state = Atom.create({ count: 0 })\n\n// track any changes to the app's state and log them to console\nstate.subscribe(x =\u003e {\n  console.log(`New app state: ${JSON.stringify(x)}`)\n})\n\n// render the app\nconst root = createRoot(document.getElementById('app'))\nroot.render(\u003cApp state={state} /\u003e)\n```\n\nYou can play with this example online [on CodeSandbox](https://codesandbox.io/s/black-bash-u6l9x).\n\nThere's also a more elaborate version of [this example](packages/examples/all/src/counter/index.tsx), as well as some other examples, in the [examples directory](packages/examples).\n\n# Installation\n\n```bash\nyarn add @grammarly/focal-atom @grammarly/focal\n# or\nyarn add @grammarly/focal-atom\n\n```\n\nor\n\n```bash\nnpm install --save @grammarly/focal-atom @grammarly/focal\n# or\nnpm install --save @grammarly/focal-atom\n```\n\nIt is important to satisfy the RxJS peer dependency (required for `instanceof Observable` tests to work correctly).\n\nAlso note, that for `npm`-based packages you will need npm 3.x. For Focal to work properly, you need to:\n\n- have the same version of RxJS installed in your package (listed as a peer dependency in Focal)\n- have RxJS installed in an npm 3.x way so that it is not duplicated in your app's node_modules and Focal's node_modules\n\n# Tutorial\n\nThe example above might be a bit too overwhelming. Let's go over the main concepts bit by bit.\n\n## Reactive variables\n\nIn Focal, state is stored in `Atom\u003cT\u003e`s. `Atom\u003cT\u003e` is a data cell that holds a single immutable value, which you can read and write to:\n\n```typescript\nimport { Atom } from '@grammarly/focal-atom'\n\n// create an Atom\u003cnumber\u003e with initial value of 0\nconst count = Atom.create(0)\n\n// output the current value\nconsole.log(count.get())\n// =\u003e 0\n\n// set 5 as the new value\ncount.set(5)\n\nconsole.log(count.get())\n// =\u003e 5\n\n// modify the value: set a new value which is based on current value\ncount.modify(x =\u003e x + 1)\n\nconsole.log(count.get())\n// =\u003e 6\n```\n\nYou can also track (get notified of) changes that happen to an `Atom\u003cT\u003e`'s value. In this sense, an `Atom\u003cT\u003e` can be thought of as a _reactive variable_:\n\n```typescript\nimport { Atom } from '@grammarly/focal-atom'\n\nconst count = Atom.create(0)\n\n// subscribe to changes of count's value, outputting a new value to the\n// console each time\n// NOTE how this will immediately output the current value\ncount.subscribe(x =\u003e {\n  console.log(x)\n})\n// =\u003e 0\n\nconsole.log(count.get())\n// =\u003e 0\n\n// set a new value – it will get written to the console output\ncount.set(5)\n// =\u003e 5\n\ncount.modify(x =\u003e x + 1)\n// =\u003e 6\n```\n\n## Atom properties\n\nEvery atom is expected to satisfy these properties:\n\n1. When `.subscribe`d to, emit the current value immediately.\n2. Don't emit a value if it is equal to the current value.\n\n## Single source of truth\n\n`Atom\u003cT\u003e`s are used as a source of application state in Focal. There are more than one way in which you can create an `Atom\u003cT\u003e`, with `Atom.create` being the one that you use to create the application state root. Ideally, we want for the application state to come from a single source of truth. (We will talk about managing application state in this fashion below).\n\nAlthough you can create as many `Atom\u003cT\u003e`s through `Atom.create` as you need, it generally should be avoided. The problem with having several (or many) sources of application state is that you may end up with different sorts of dependencies between these state sources, and there is no way to update several `Atom\u003cT\u003e`s at the same time. This can lead to inconsistency between the parts of your application state.\n\n## Data binding\n\nWe have learned how to create, change and track application state. Now, for it to be useful in a React user interface, we need a way to display this data.\n\nFocal allows you to directly embed `Atom\u003cT\u003e`s in JSX code. In practice, this works similar to data binding in frameworks like Angular. There are differences, though:\n\n- In Focal, you use regular code (TypeScript or JavaScript), and not a template engine syntax (like in Vue.js), to describe your data. There's no magic happening at the syntax level, so you can use all the standard language tools that you already have.\n- Since Focal data bindings are ordinary TypeScript (or JavaScript) expressions, you can continue using the same IDE features like autocompletion, go to definition, rename refactoring, find usages, etc. This makes UI layout code maintenance easier compared to a template engine.\n- You can also take advantage of your existing static analysis tools (e.g. the type checking of TypeScript compiler). This way, your UI code can be just as reliable as any other code.\n- The change of data (an `Atom\u003cT\u003e`) triggers component render, and not the other way around (e.g. when component render triggers data evaluation). You also usually don't think about when a component should get rendered – this is handled automatically.\n\nLet's see what it looks like in code:\n\n```typescript\nimport * as React from 'react'\nimport * as ReactDOM from 'react-dom'\nimport { F, Atom } from '@grammarly/focal'\n\n// create our state\nconst count = Atom.create(0)\n\n// define a stateless React component that will take\n// an Atom\u003cnumber\u003e in its props\nconst Counter = (props: { count: Atom\u003cnumber\u003e }) =\u003e\n  \u003cF.div\u003e\n    {/* embed the state atom directly in JSX */}\n    Count: {count}\n  \u003c/F.div\u003e\n\n// mount the component onto DOM\nconst root = createRoot(document.getElementById('test'))\nroot.render(\u003cCounter count={count} /\u003e)\n\n// =\u003e \u003cdiv\u003eCount: 0\u003c/div\u003e\n```\n\nHow is this different from regular React?\n\nInstead of using a `\u003cdiv /\u003e`, we used an `\u003cF.div /\u003e`. In React, you can already embed code in JSX, but it's mostly restricted to things that can be converted to a string and other React elements.\n\n`F`-components are different. `F` is a namespace of the so-called lifted components that mirror React's intrinsic components, but which can also accept `Atom\u003cT\u003e`s (additionally to what React already allows) in their props.\n\nRecall that a React JSX element child content gets interpreted as the `children` prop, so this means that Focal also supports embedding `Atom\u003cT\u003e`s in component child content – that's what we did.\n\nNow let's get the application state to update:\n\n```typescript\n// This line below will modify the current value of the `count` atom,\n// which we used in the `Counter` component. Modifying the state which was\n// used in a component will make the component update:\ncount.set(5)\n\n// =\u003e \u003cdiv\u003eCount: 5\u003c/div\u003e\n```\n\nYou may have noticed that we didn't update any React component state, yet the `\u003cCounter /\u003e` component somehow has new content now. In fact, as far as React is concerned, neither props nor state of the `\u003cCounter /\u003e` component have changed, so this component is not rendered when the counter state is changed.\n\nThis content update is handled in the `\u003cF.div /\u003e` component, which is also true for all lifted (a.k.a. `F`) components. An `F`-component will `.subscribe` to all of its `Atom\u003cT\u003e` props and render every time a prop value has changed.\n\nSo technically, while the `\u003cCounter /\u003e` is not rendered when the `count`'s value is changed, its child `\u003cF.div /\u003e` _is_ rendered.\n\nNow let's make our counter component a bit more complex:\n\n```typescript\n// a spiced up version of the counter component\nconst Counter = (props: { count: Atom\u003cnumber\u003e }) =\u003e\n  \u003cF.div\u003e\n    Count: {count}.\n    {/* say whether the count number is odd or even */}\n    That's an {count.view(x =\u003e x % 2 === 0 ? 'even' : 'odd')} number!\n  \u003c/F.div\u003e\n\n// =\u003e \u003cdiv\u003eCount: 5. That's an odd number!\u003c/div\u003e\n```\n\nWe've added this `That's an odd number!` line (which will also say `even` for even numbers) by creating a _view_ of our state atom.\n\nTo create a view means to create an atom which shows its state in a modified way that is defined by the view function. It's not a lot different than `map`ping over an [array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) or an [observable](https://rxjs.dev/api/operators/map), actually. The main difference is that just like the source atom, the derived atom (a view) will only emit a new value if it is not equal to the current value.\n\nLet's try one more thing:\n\n```typescript\nconst Counter = (props: { count: Atom\u003cnumber\u003e }) =\u003e\n  \u003cF.div\n    style={{\n      // make the background progressively more red as the count number increases\n      'background-color': count.view(x =\u003e `rgba(255, 0, 0, ${Math.min(16 * x, 255)})`)\n    }}\n  \u003e\n    Count: {count}.\n    That's an {count.view(x =\u003e x % 2 === 0 ? 'even' : 'odd')} number!\n  \u003c/F.div\u003e\n\n// =\u003e \u003cdiv style=\"{'background-color': 'rgba(255, 0, 0, 80)'}\"\u003eCount: 5. That's an odd number!\u003c/div\u003e\n```\n\nHere, we used our state atom to create a _dynamic style_ for our component. So, as you can see, you can use atoms almost anywhere with `F`-components, which makes it easy to make your UI dependent on state in a declarative way.\n\n## Composition\n\nWe have learned how to declaratively create UI layouts that depend on application state. Now, to make a bigger and more complex application with these tools, and keep this application from falling apart, we need two things:\n\n1. Have the application state come from a single place (a single atom), so that when different parts of the application interact with each other, these interactions can't fall out of sync with each other and the application state is consistent as a whole.\n2. Split the application state into parts so that we can create our app layout from small UI components that don't have to know about the whole application state.\n\nThese two requirements may look mutually exclusive at first, and here's when lenses finally come into play.\n\n## Lens\n\nA quick refresher on what a lens is:\n\n- an abstraction to read and write a _part_ of _immutable_ data\n- a combination of a getter and a setter function\n\nIn TypeScript, a lens can be expressed as this generic interface:\n\n```typescript\ninterface Lens\u003cTSource, T\u003e {\n  get(source: TSource): T\n  set(newValue: T, source: TSource): TSource\n}\n```\n\nAnd an example usage:\n\n```typescript\nimport { Lens } from '@grammarly/focal-atom'\n\n// an object that we want to operate on\nconst obj = {\n  a: 5\n}\n\n// our example lens that looks at the object's `a` property\nconst a = Lens.create(\n  // provide the getter: returns the `a` property\n  (obj: { a: number }) =\u003e obj.a,\n  // and the setter: returns a new object with the `a` property updated to a new value\n  (newValue, obj) =\u003e ({ ...obj, a: newValue })\n)\n\n// read through the lens\nconsole.log(a.get(obj))\n// =\u003e 5\n\n// write through the lens\nconsole.log(a.set(6, obj))\n// =\u003e { a: 6 }\n```\n\nNote how we get a fresh object back when we use the `.set` method: as we're working without mutations, we create a new object when we want to `.set` some part of it through a lens.\n\nThis doesn't look very useful yet, though. After all, why don't we just do `obj.a` and `{ ...obj, a: 6 }` every time we need to?\n\nBut imagine that your object is lot more complex than that (e.g. `{ a: { b: { c: 5 } } }`), and that it's just a part of some even bigger object:\n\n```typescript\nconst bigobj = {\n  one: { a: { b: { c: 5 } } },\n  two: { a: { b: { c: 6 } } }\n}\n```\n\nOne powerful feature of lenses is that they can be composed (chained together). Assume you have defined a lens to drill down to the `c` property of the `{ a: { b: { c: 5 } } }` object. Now you can reuse that to work on both (`one` and `two`) parts of the `bigobj` object:\n\n```typescript\n// a lens that is focused on a deeply nested property `c` of an `{ a: { b: { c: 5 } } }` object\nconst abc: Lens\u003c...\u003e = ...\n\n// a lens that looks at the `one` part of `bigobj`\nconst one: Lens\u003ctypeof bigobj, ...\u003e = ...\n\n// a lens that looks at the `two` part of `bigobj`\nconst two: Lens\u003ctypeof bigobj, ...\u003e = ...\n\n// compose `one` and `two` with `abc` so we can work on `c` of `{ one: { a: { b: { c: 5 } } } }`\n// and `{ two: { a: { b: { c: 5 } } } }`\nconst oneC = one.compose(abc)\nconst twoC = two.compose(abc)\n\nconsole.log(oneC.get(bigobj))\n// =\u003e 5\n\nconsole.log(twoC.get(bigobj))\n// =\u003e 6\n\nconsole.log(oneC.set(7, bigobj))\n// =\u003e { one: { a: { b: { c: 7 } } }, two: { a: { b: { c: 6 } } } }\n```\n\nFocal also gives you the ability to define these lenses quite conveniently:\n\n```typescript\n// create the above lenses by providing key names\nconst abc = Lens.key\u003ctypeof bigobj.one\u003e()('a', 'b', 'c')\n\nconst one = Lens.key\u003ctypeof bigobj\u003e()('one')\n\nconst two = Lens.key\u003ctypeof bigobj\u003e()('two')\n\n// Note the extra nullary call. It is there for better type inference. To create a type safe\n// lens we need to tell the compiler both the type of source and the type of destination property.\n// Unlike with Atom.lens, the compiler doesn't yet know the type of the source data, so we have to\n// explicitly state it. The compiler then can easily infer the type of the destination property by\n// its name.\n//\n// We need the nullary call because we have two generic type parameters, and want one of them\n// explicit and another inferred. So the nullary call is there only to supply the type argument\n// for the source data type.\n```\n\nThe best part about this is that it is completely type safe, and all of the IDE tools (like auto-completion, refactoring, etc.) will just work.\n\nIt may seem strange that we only provide the getter function when a lens should be able to also set the focused value. And it is strange indeed – this is a place where we have a bit of magic going on. But this should only be regarded as an implementation detail, as with advances in the TypeScript compiler it may become obsolete in the future.\n\n_For a quick explanation, we use a similar trick that was (and probably is) a standard practice in WPF to implement a [type safe `INotifyPropertyChanged` interface](https://mfelicio.com/2010/01/10/safe-usage-of-inotifypropertychanged-in-mvvm-scenarios/). We convert the getter function to a string (by calling `.toString()`) and then parse the property access path from the function's source code. It's a hacky way to do this and has a lot of obvious limitations, but it helps a lot._\n\n## More about lenses\n\nHopefully, the section above can give you some feeling of the power of lenses, as there is a lot more you can do with this abstraction. It would not be possible to cover all of the interesting parts in this short tutorial, though.\n\nUnfortunately, most articles and tutorials on lenses are written in context of the [Haskell](https://www.haskell.org/) programming language. This is because lenses are most explored in Haskell. However, they are also used in quite a number of other languages, including Scala, F#, OCaml, PureScript, Elm and probably many others.\n\n- [Program imperatively using Haskell lenses](http://www.haskellforall.com/2013/05/program-imperatively-using-haskell.html)\n- [WikiBooks article on Haskell lenses](https://en.wikibooks.org/wiki/Haskell/Lenses_and_functional_references)\n- [School of Haskell lens tutorial](https://www.schoolofhaskell.com/school/to-infinity-and-beyond/pick-of-the-week/a-little-lens-starter-tutorial)\n- [lens over tea blog series](https://artyom.me/lens-over-tea-1)\n\n## Atoms and lenses\n\nOkay, let's get back on track. So far we have learned how to work with application state and embed it in our UI layout code.\n\nWe have also learned how to abstract operations on immutable data, to conveniently operate on parts of large immutable objects. And this is exactly what we will need to split our application's state into parts. We want to build our application in a way so that each UI component can work with only relevant parts of the whole application state.\n\nWe can accomplish this by combining atoms with lenses, making _lensed atoms_.\n\nA lensed atom is just an `Atom\u003cT\u003e`, in sense that on the outside it looks and behaves just like another atom. The difference is in how it is created: a lensed atom operates on a part of some other atom's state. This means that if you `.set` or `.modify` a lensed atom's value, the part of the source atom's value at which this lensed atom's lens is focused will also change. For example:\n\n```typescript\nimport { Atom, Lens } from '@grammarly/focal-atom'\n\n// create an atom to hold our object\nconst obj = Atom.create({\n  a: 5\n})\n\n// create a lens to look at the `a` property of the object\nconst a = Lens.key\u003ctypeof obj\u003e()('a')\n\n// create a lensed atom that will hold a value of the `a` property of our object\nconst lensed = obj.lens(a)\n\nconsole.log(obj.get())\n// =\u003e { a: 5 }\n\nconsole.log(lensed.get())\n// =\u003e 5\n\n// set a new value to the lensed atom\nlensed.set(6)\n\nconsole.log(obj.get())\n// =\u003e { a: 6 }\n```\n\nNote how the source atom's value has changed when we set a new value to the lensed atom – that's it. There's also a neat shortcut to create lensed atoms:\n\n```typescript\nconst lensed = obj.lens('a')\n```\n\nWe don't need to explicitly create a `Lens` – atom's `lens` method already has a couple of overloads to create lensed atoms on the spot. Also note that we didn't need to add the `typeof obj` type annotation here: the compiler already knows the type of data we're operating on (from the type of `obj`, which in this case is `Atom\u003c{ a: number }`) and can infer the type of `x` for us.\n\nArmed with this, it should now be possible for us to decompose the single-source-of-truth state atom of our application into small parts, suitable to be used in individual UI components. Let's try this with our counter example from above:\n\n```typescript\nimport * as React from 'react'\nimport * as ReactDOM from 'react-dom'\nimport { Atom, F } from '@grammarly/focal'\n\n// application state\nconst state = Atom.create({\n  count: 0\n})\n\n// the counter component from before\nconst Counter = (props: { count: Atom\u003cnumber\u003e }) =\u003e\n  \u003cF.div\u003e\n    Count: {props.count}.\n\n    \u003cbutton onClick={() =\u003e props.count.modify(x =\u003e x + 1)}\u003e\n      Click again!\n    \u003c/button\u003e\n  \u003c/F.div\u003e\n\n// the app component which takes the whole app state from the\n// `state` prop\nconst App = (props: { state: Atom\u003c{ count: number }\u003e }) =\u003e\n  \u003cdiv\u003e\n    Hi, here's a counter:\n\n    {/*\n      this is where we take apart the app state and give only a part of it\n      to the counter component:\n    */}\n    \u003cCounter count={props.state.lens('count')} /\u003e\n  \u003c/div\u003e\n```\n\nAnd this concludes our tutorial on Focal basics!\n\nHopefully, it is now more clear how all of this comes together. Please also make sure to check out some of the [other examples](packages/examples) – build them and play around to get a better feel for what you can do.\n\n# Framework?\n\nFocal is not a framework, in the sense that you are not restricted to a particular way of writing your whole application. Focal has an imperative interface (remember that you can use the `.set` and `.modify` methods of atom) and can play nicely with React components of any nature. This means that using Focal is optional within different parts of the same application.\n\n# Performance\n\nAlthough we've yet to establish a comprehensive set of benchmarks, so far Focal has shown performance at least on par with plain React on examples such as TodoMVC.\n\nGenerally, when an `Atom\u003cT\u003e`/`Observable\u003cT\u003e` that was embedded into a React component emits a new value, only relevant parts of the component are updated. This means that if you have a complex React component with an actively changing value somewhere deep inside its visual tree, only that part will get updated, not the whole component. In many cases, this can make it a lot easier to optimize for VDOM re-computations.\n\n# Commercial uses\n\nWe used Focal at [Grammarly](https://www.grammarly.com/) to build our [online writing app](https://app.grammarly.com/).\n\n![Grammarly logo](https://d1zw7v9lpbbx9f.cloudfront.net/static/files/997ea3a3690bda688b2a6d7407bb5eb9/logo.svg)\n\n# JavaScript support\n\nAlthough technically it should be possible to use Focal in a JavaScript project, we haven't tried that yet. So please feel free to open any issues you might have with this kind of setup.\n\n# Prior art\n\nFocal started as a TypeScript port of [Calmm](https://github.com/calmm-js/), but along the way we ended up with some significant differences.\n\nFrom the start, we focused more on immediate developer productivity and type safety. Because of this, many things were simplified, as it was either hard to make the types work out nicely at the time (TypeScript 1.8) or difficult to make the APIs intuitive and easy to use for people familiar with React but new to functional programming.\n\n### Differences from Calmm\n\n- Calmm is modular in terms of consisting of several independent libraries. We didn't have the need for the modularity since we only had a single use case, so we keep everything together under a single library.\n- Calmm originally makes heavy use of [Ramda](http://ramdajs.com/), currying and partially applying things here and there. This was hard to type right, so we decided to drop this practice. Although I think today it is probably easier, with advancements in the TypeScript compiler, so maybe this could be an interesting topic to explore.\n- Calmm was also originally using Ramda's representation of lens, which is the [van Laarhoven representation](http://www.twanvl.nl/blog/haskell/cps-functional-references). Instead, we opted for a naїve approach of representing lens with a getter/setter pair. This has worked fine for us so far, since we haven't needed to do any traversals or polymorphic updates yet. Perhaps we should reconsider this once again some time.\n- The main Calmm implementation ([kefir.atom](https://github.com/calmm-js/kefir.atom) and [kefir.react.html](https://github.com/calmm-js/kefir.react.html)) was based on [Kefir](http://rpominov.github.io/kefir/) observables. We started with Kefir as well, but soon enough migrated to [RxJS 5.x](https://github.com/ReactiveX/rxjs). The main reason was that RxJS was more fully featured and supported some operations on observables that Kefir didn't.\n\n# Contribution\n\nThis repository uses `changesets` to automate versioning of the packages.\n\nWhen you contribute a change to the library that should be mentioned in the changelog, please use the following command:\n\n```sh\nyarn changeset\n```\n\nThen following the prompt describe packages that are affected and the description of the change. Created markdown files should be included to the pull request.\n\n# License\n\nCopyright 2019 Grammarly, Inc.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgrammarly%2Ffocal","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgrammarly%2Ffocal","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgrammarly%2Ffocal/lists"}