├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .npmignore ├── .nvmrc ├── .prettierignore ├── .prettierrc.cjs ├── LICENSE ├── README.md ├── dist ├── index.d.ts ├── index.es.js └── index.umd.js ├── lib ├── core.js ├── index.js ├── tests │ ├── components │ │ ├── TestForm.jsx │ │ ├── defaultValues.js │ │ ├── options.js │ │ └── schema.js │ ├── core.test.js │ ├── useForm-arrays │ │ ├── UnitTestForm.jsx │ │ └── useForm-arrays.test.jsx │ ├── useForm.test.jsx │ ├── useStableRef.test.jsx │ ├── yupResolver.test.js │ └── zodResolver.test.js ├── useForm.jsx ├── useStableRef.jsx ├── yupResolver.js └── zodResolver.js ├── package.json ├── public └── index.d.ts ├── stats.html ├── vite.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .prettierrc.js 4 | .eslintrc.js 5 | env.d.ts -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | settings: { 3 | react: { 4 | version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use. 5 | }, 6 | 'import/resolver': { 7 | node: { 8 | paths: ['src'], 9 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 10 | }, 11 | }, 12 | }, 13 | env: { 14 | browser: true, 15 | es6: true, 16 | node: true, 17 | }, 18 | extends: ['eslint:recommended', 'airbnb', 'prettier', 'eslint-config-prettier'], 19 | plugins: ['react-hooks'], 20 | rules: { 21 | 'import/no-extraneous-dependencies': 0, 22 | 'jsx-a11y/anchor-is-valid': 0, 23 | 'jsx-a11y/click-events-have-key-events': 0, 24 | 'jsx-a11y/no-static-element-interactions': 0, 25 | 'jsx-a11y/no-noninteractive-element-interactions': 0, 26 | 'react/jsx-filename-extension': 0, 27 | 'import/no-cycle': 0, 28 | 'no-console': 'off', 29 | 'no-shadow': 'off', 30 | 'no-plusplus': 'off', 31 | 'react/jsx-indent': [1, 'tab', { checkAttributes: false, indentLogicalExpressions: true }], 32 | 'react/jsx-props-no-spreading': 0, 33 | 'react/jsx-one-expression-per-line': 0, 34 | 'react/react-in-jsx-scope': 0, // vite does this automatically 35 | 'no-param-reassign': 0, 36 | 'react/forbid-prop-types': [0], 37 | 'react/prop-types': [0], 38 | 'react/require-default-props': [0], 39 | 'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks 40 | 'react-hooks/exhaustive-deps': 'warn', // Checks effect dependencies 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js text eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist-ssr 4 | *.local 5 | 6 | # binaries 7 | bin/ 8 | 9 | # editors 10 | *.swp 11 | .idea/ 12 | .vs/ 13 | .vscode/ 14 | .DS_Store 15 | 16 | # libs 17 | node_modules/* 18 | 19 | TODO.txt -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v21.7.3 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .prettierrc.js 4 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | //"module": "commonjs" 2 | 3 | module.exports = { 4 | parser: 'babel', 5 | printWidth: 170, // wrap lines at 6 | tabWidth: 4, // indentation 7 | useTabs: true, // tabs instead of spaces 8 | semi: true, // add semicolons to end of statements 9 | singleQuote: true, // force single quotes where possible 10 | quoteProps: 'as-needed', // only required for some props 11 | jsxSingleQuote: false, // jsx prop quotes 12 | trailingComma: 'all', // es5? 13 | bracketSpacing: true, // { foo: bar } instead of {foo: bar} 14 | jsxBracketSameLine: false, // https://prettier.io/docs/en/options.html#jsx-brackets 15 | arrowParens: 'avoid', // x => {} 16 | // "rangeStart": 0, 17 | // "rangeEnd": 0, 18 | // "parser": "babel", 19 | // "filepath": "", 20 | requirePragma: false, 21 | //"insertPragma": false, 22 | proseWrap: 'preserve', 23 | htmlWhitespaceSensitivity: 'css', 24 | embeddedLanguageFormatting: 'auto', 25 | endOfLine: 'auto', 26 | vueIndentScriptAndStyle: false, 27 | }; 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # useForm 2 | React forms hook, lightweight alternative to existing frameworks. 3 | 4 | [See full demo](https://k7s4y.csb.app/) 5 | 6 | Concerned about performance? Try this demo, optimized with with React.memo: 7 | 8 | [400 form inputs](https://7izw4f.csb.app/) 9 | 10 | ## Installation 11 | ```bash 12 | npm install --save @rvision/use-form 13 | ``` 14 | or 15 | ```bash 16 | yarn add @rvision/use-form 17 | ``` 18 | 19 | ## Quickstart: basic usage 20 | ```jsx 21 | const defaultValues = { 22 | firstName: '', 23 | lastName: '', 24 | email: '', 25 | agree: false 26 | }; 27 | 28 | const Form = () => { 29 | const { 30 | register, 31 | handleSubmit, 32 | } = useForm({ 33 | defaultValues 34 | }); 35 | 36 | const onSubmit = values => console.log(values); // handles form submit: call API, etc. 37 | 38 | return ( 39 |
40 | 44 | 48 | 52 | 56 | 59 | 60 | ); 61 | } 62 | ``` 63 | 64 | ## Another form library? 65 | Recently, [react-hook-form](https://react-hook-form.com/) gained stellar popularity because it's easy to use and it uses uncontrolled components, making it very performant. And it works fine. Unless you have to deal with complex, dynamic forms, then you'll have to use ```formState, control, Controller, useController, useFormContext, watch, useWatch, useFormState, useFieldArray``` and on and on. This imperative not to "re-render" form component ended up in having multiple useEffects and derived states which makes your code harder to reason about. [Removing unnecessary Effects will make your code easier to follow, faster to run, and less error-prone.](https://beta.reactjs.org/learn/escape-hatches#you-might-not-need-an-effect). 66 | 67 | ## Motivation 68 | I ended building this library because the code for the complex forms was absolute horror. 69 | This library works with **controlled components only**, meaning that on each keystroke in input field, whole form will be re-rendered. Performance depends on the **number** and **types** of components used. For native inputs, it is decently fast, for custom components, your mileage may vary. 70 | Note that handlers have **stable references**. This allows you to memoize parts of big forms and achieve better [rendering performance](https://7izw4f.csb.app/). If you have hundreds of inputs in your form, you're not building a form, you are building Excel clone an you should probably use uncontrolled inputs. 71 | 72 | ## Goals 73 | - **0 dependencies** 74 | - **lightweight**: ~3.5kb minified & gzipped 75 | - **simplicity**: low learning curve 76 | - **nested arrays** without hassle 77 | - **un-opinionated** - components freedom: doesn't force you to use any specific component for inputs or form, it allows use of native input fields via ```register``` and custom components via ```getValue/setValue``` functions 78 | - **stable handlers** allow easily to memoize rendering 79 | - **schema-based validation**: sync validation support for [yup](https://github.com/jquense/yup) and [zod](https://github.com/colinhacks/zod) included 80 | - **natural way** to reference to any field with regards of the initial object shape/structure. For example: 81 | 82 | ```js 83 | getValue('firstName') // 1st level property 84 | setValue('birthDate') // 1st level property 85 | getValue('departments.5.employees.3.tasks.1.name') // nested property, name of second task 86 | trigger('cities.2.population') // nested property, population of third city 87 | getValue('artists.1.albums.2') // nested property, third album of the second artist 88 | ``` 89 | 90 | Let's call this identifier ```fullPath```. Most of the methods to work with fields and errors will use this naming convention for operations. 91 | 92 | ### Hook options 93 | ```js 94 | useForm({ 95 | defaultValues: {}, 96 | mode: 'onSubmit', 97 | focusOn: fullPath, 98 | classNameError: null, 99 | shouldFocusError: false, 100 | resolver: () => {} 101 | }); 102 | ``` 103 | 104 | **Property** | **Type** | **Description** 105 | --------------- | ------------- | -------------------------------------------------------------- 106 | ```defaultValues```| ```object``` | initial form values; for new records it has to be populated with default values (e.g. empty strings, true/false, date, etc.) 107 | ```mode``` | ```'onSubmit' / 'onChange' / 'onBlur'``` | validation mode, see below 108 | ```focusOn``` | ```string``` | fullPath to registered input via (register or setRef) that will be focused when form is mounted 109 | ```classNameError``` | ```string``` | Registered fields with validation error will have this css class name appended to their className list. Errors and Error components will use this class name also 110 | ```shouldFocusError``` | ```bool``` | if field has validation error, it will focus on error field when validation mode is onBlur. Also, when form is submitted and there are errors, it will focus on the first field with validation error 111 | ```resolver``` | ```function(fieldValues)``` | validation function, yupResolver and zodResolver are provided for [yup](https://github.com/jquense/yup) and [zod](https://github.com/colinhacks/zod) or you can write your custom 112 | 113 | 114 | 115 | ### Validation 116 | **Mode** | **Description** 117 | ---------------| ------------------------------------------------------------------------------------------------------------------ 118 | | default validaton behaviour: validates when form is submitted, if there are any validation errors - when edited, fields with errors will be re-validated like ```'onChange'``` mode. Like a combination of ```'onSubmit'``` and ```'onChange'``` modes. I find this pattern suitable for most use cases. 119 | ```'onSubmit'``` | validates when form is submitted, if there are any validation errors - when edited, errors will be removed for fields with errors and revalidated on next form submit 120 | ```'onChange'``` | validates form fields on each change 121 | ```'onBlur'``` | validates fields when focused out. For registered inputs, it will trap user to the field if ```shouldFocusError``` option is set to true 122 | 123 | 124 | ### Returned props 125 | ```js 126 | const { 127 | register, 128 | getValue, 129 | setValue, 130 | onChange, 131 | onBlur, 132 | getRef, 133 | setRef, 134 | trigger, 135 | handleSubmit, 136 | hasError, 137 | getError, 138 | clearError, 139 | setErrors, 140 | array: { 141 | clear, 142 | append, 143 | prepend, 144 | remove, 145 | swap, 146 | insert, 147 | }, 148 | key, 149 | Error, 150 | Errors, 151 | formState: { 152 | errors, 153 | isValid, 154 | isTouched, 155 | isDirty, 156 | hadError, 157 | reset, 158 | }, 159 | } = useForm(options); 160 | ``` 161 | 162 | #### register(fullPath: string, className: string ) 163 | 164 | field registration method for native inputs; uses ```fullPath``` concept to identify the field in the object hierarchy; className is used with classNameError setting if field has validation error 165 | ```jsx 166 | {/* Examples: */} 167 | 168 | {/* or */} 169 | 170 | {/* or */} 171 | 172 | {/* or */} 173 | 174 | {/* or */} 175 | 179 | ``` 180 | **NOTE** 181 | Only input type that doesn't allow automatic registration is file type, because of the browser security. You can only use ```onChange``` event to set the value, but you cannot display it. Here is an example how to deal with those fields: 182 | ```jsx 183 |
184 | {getValue('files').map((file, idx) => { 185 | return ( 186 | 187 |
188 | 194 |
195 | 196 |
197 | ); 198 | })} 199 |
200 | ``` 201 | 202 | 203 | #### getValue(fullPath: string) : any 204 | 205 | method to get value of the field, uses ```fullPath``` concept as field identifier. use it for custom components 206 | ```jsx 207 | 208 | ``` 209 | 210 | #### setValue(fullPath: string, newValue: any, shouldRevalidate: true) 211 | 212 | method to set value of the field, uses ```fullPath``` concept as field identifier. promise returns new values/errors object. use it for custom components. 213 | ```jsx 214 | { 216 | setValue('movies.3.releaseDate', date, false); 217 | }} 218 | /> 219 | {/* or */} 220 | Clear movie list{/* NOTE:use array.clear if you want to preserve existing validation errors */} 221 | ``` 222 | 223 | #### onChange(event: React.SyntheticEvent) 224 | 225 | if you need debouncing or additional logic when field value is changed, use ```'onChange'``` method; it overrides default method set by register 226 | ```jsx 227 | { 231 | // additional logic 232 | onChange(e); 233 | }} 234 | /> 235 | ``` 236 | 237 | #### onBlur(event: React.SyntheticEvent) 238 | 239 | same, but for ```onBlur``` event 240 | ```jsx 241 | { 245 | //additional logic 246 | onBlur(e); 247 | }} 248 | /> 249 | ``` 250 | 251 | #### getRef(fullPath: string) 252 | 253 | helper method to get reference (ref) to the native input, uses ```fullPath``` concept as field identifier 254 | ```jsx 255 | { 256 | getRef(`movies[${i}].coStars[0].firstName`).focus() 257 | }}> 258 | ``` 259 | 260 | #### setRef(fullPath: string, element: ref-able DOM element) 261 | 262 | helper method to store reference (ref) to the native input, uses ```fullPath``` concept as field identifier; Use it for storing refs for custom components, this way they can be focusable when clicking on the error in the list of errors from `````` component 263 | ```jsx 264 | { 267 | if (ref && ref.input) { 268 | setRef('birthDate', ref.input); 269 | } 270 | }} 271 | onChange={date => { 272 | setValue('birthDate', date); 273 | }} 274 | /> 275 | {/* or */} 276 | 101 | {optionsTitle.map(option => ( 102 | 105 | ))} 106 | 107 | 108 | 109 | 110 |
111 | 115 | 116 |
117 |
118 | 123 |
124 |
125 | 143 | 144 |
145 | 146 | 147 |
148 |
149 |
150 | 151 | 163 | 164 |
165 | 166 | 177 | 178 |

(dynamic rows, 1st level)

179 |
180 | 181 |
182 | {(getValue('albums') || []).map((album, idx) => ( 183 | 184 |
185 | 189 |
190 |
191 | 196 |
197 |
198 | 217 |
218 |
219 | 228 |
229 |
230 | 231 | ))} 232 |
233 |
234 |
235 |
236 |
237 | 238 | 239 | 254 | 255 | 256 |
257 | 258 | 267 | 268 |

(dynamic nested arrays)

269 |
270 | 271 |
272 | {(getValue('movies') || []).map((movie, idx) => { 273 | const k = key(movie); 274 | return ( 275 | 276 |
277 | 281 |
282 |
283 | 288 |
289 |
290 | 309 |
310 | {/* */} 311 |
312 | 323 |
324 | Metacritic score: {getValue(`movies[${idx}].metaCritic`)}% 325 |
326 |
327 | 336 |
337 | 338 |
339 | 340 |
341 |
342 |
343 |
344 |
349 |
441 | 442 |
443 | 448 | 449 |
450 | 451 |
452 | 461 |
462 |
463 | 464 | ))} 465 |
466 |
467 | 468 | ); 469 | })} 470 |
471 |
472 |
473 |
474 |
475 | 476 |

(how to use <select /> multiple, react-select or array of checkboxes)

477 |
478 |
479 |
480 |
481 |
482 | 489 | 490 |
491 |
492 |
493 |
494 |
495 | {optionsOccupation.map(option => ( 496 | 497 | 514 |
515 |
516 | ))} 517 | 518 |
519 |
520 | 521 |
522 |