Skip to content

Commit

Permalink
add docs section on hooks linting
Browse files Browse the repository at this point in the history
  • Loading branch information
roman01la committed Jun 22, 2022
1 parent 1dbb7d9 commit baa7b90
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ UIx v1 is in [roman01la/uix](https://github.com/roman01la/uix) repo
- [Migrating from Reagent](/docs/migrating-from-reagent.md)
- [Hot reloading](/docs/hot-reloading.md)
- [React DevTools](/docs/react-devtools.md)
- [Hooks linter](/docs/hooks-linter.md)

## Testing

Expand Down
152 changes: 152 additions & 0 deletions docs/hooks-linter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Hooks linter

UIx has a built-in linter that will help you to use React Hooks correctly. The linter is built into `defui` and default `uix.core/*` hooks, it implements a set of rules from React's official [ESLint plugin](https://reactjs.org/docs/hooks-rules.html).

While in original ESLint plugin there are rules that can be considered as suggestions and thus reported as warnings, most of the rules implemented in UIx should be always followed, breaking them will lead to bugs in UI. For this reason in UIx a broken rule will fail a build so that it's impossible to build a project with problematic behaviour in UI components.

## What's the rule of thumb to use Hooks correctly?

Call hooks at the top level in component's body

```clojure
;; bad
(defui component [{:keys [active?]}]
(when active?
(use-effect ...))
...))

;; good
(defui component [{:keys [active?]}]
(use-effect
(fn []
(when active?
...)))
...))
```

List all necessary dependencies in deps vector for hooks that require dependencies

```clojure
;; bad
(defui component [{:keys [active? id]}]
(use-effect
(fn []
(when active?
(rf/dispatch :user/set-id {:id id})))
[active?])
...))

;; good
(defui component [{:keys [active? id]}]
(use-effect
(fn []
(when active?
(rf/dispatch :user/set-id {:id id})))
[active? id])
...))
```

## What type of errors UIx is able to catch?

### Hooks called inside of conditions or iterating functions

The rule here is to call function at the top level of component's body.

```clojure
(defui component [{:keys [active?]}]
(when active?
(use-effect ...)) ;; error
...))

(defui component [{:keys [items]}]
(for [item items]
($ list-item
{:item item
;; error
:on-click (use-callback #(rf/dispatch %) [item])}))))
```

### A hook doesn't meet its dependencies requirements

> This rule is currently experimental and disabled, it's possible to opt-in by adding `^:lint-deps` meta onto deps vector
This rule will check for missing and unnecessary dependencies and suggest a correct deps vector.

```clojure
(defui component [{:keys [active? id]}]
(use-effect
(fn []
(when active?
(rf/dispatch :user/set-id {:id id})))
[active?]) ;; error, update deps vector to [active? id]
...))
```

### Unsafe set-state in effect hook without dependencies

This type of code leads to infinite loop of updates in components.

```clojure
(defui component [{:keys [active? id]}]
(let [[value set-value] (use-state 0)]
(use-effect
(fn []
(set-value (inc value)))))) ;; error

(defui component [{:keys [active? id]}]
(let [[value set-value] (use-state 0)]
(use-effect
(fn []
(set-value (inc value)))
[value]))) ;; fix: only run hook when value changes
```

### A hook is being passed something as deps that is not a vector literal

Deps should be always a vector literal of constant size, React doesn't allow deps to be of dynamic length because it causes issues in UI components.

```clojure
;; incorrect
(defui component [{:keys [labels]}]
(let [dimensions (use-memo #(measure-labels labels) labels)]
...))

;; correct
(defui component [{:keys [labels]}]
(let [dimensions (use-memo #(measure-labels labels) [labels])]
...))
```

### A hook is being passed deps as JS array instead of a vector

This is UIx specific, since UIx is a Clojure wrapper it expects a vector of deps instead of JS array to be more idiomatic with repsect to Clojure.

```clojure
(defui component [{:keys [html]}]
(let [html (use-memo #(sanitize-html html) #js [html])] ;; incorrect
...))

(defui component [{:keys [html]}]
(let [html (use-memo #(sanitize-html html) [html])] ;; correct
...))
```

### A function reference is passed into a hook instead of an inline function

This won't cause actual bugs, but it prevents further type checking to determine if the hook satisfies dependency requirements, thus it's encouraged to use inline function instead. Note that linter might improve in the future and this rule will be depreacated.

```clojure
(defui component [{:keys [active? id]}]
(let [do-something (fn []
(when active?
(rf/dispatch :user/set-id {:id id})))]
;; deps are correct, but it still gonna error
(use-effect do-something [active? id])))

(defui component [{:keys [active? id]}]
(let [do-something (fn [active? id]
(when active?
(rf/dispatch :user/set-id {:id id})))]
;; now linter is able to check whether the effect meets deps requirements correctly
(use-effect #(do-something active? id) [active? id])))
```

0 comments on commit baa7b90

Please sign in to comment.