React-hook-form: Custom yup validation with useFieldArray doesn't always trigger re-render

Created on 24 Jul 2020  路  56Comments  路  Source: react-hook-form/react-hook-form

Describe the bug
I have custom yup validation which checks if there are same names in field array and returns false if there are 2 or more items with the same name. This works fine if you add 2 items to field array. When you add third one, validation is always "1 step behind" after rendering. It is validated, but render is not triggered so the error css is not displayed for the last field that is invalid.

To Reproduce
Steps to reproduce the behavior:

  1. Go to sandbox link
  2. Click on "+ Custom job" button at the top of the page, new job is added
  3. Click on "+ Custom job" button at the top of the page, new job is added and validation occurred, both new jobs with empty names are marked with red border to indicate error
  4. Click on "+ Custom job" button at the top of the page, third new job is added, but validation didn't trigger re-render and last invalid custom job doesn't have red border. This pattern happens each time with any dynamically added custom job, but only for third added item. If you add more, always the last one is not marked as invalid. There is also log in the console when validation occurs and when rendering occurs.

Codesandbox link
https://codesandbox.io/s/react-hook-form-usefieldarray-owyo0?file=/src/index.js

Expected behavior
All new custom jobs with empty names should be marked red, re-render should be triggered after each validation.

question

All 56 comments

so you mean the moment you hit +, you should see the error highlight?

Well, that's how it works when you add second job, both of them are error highlighted. When you add third one, that one is not. When you add fourth, third one is highlighted, but fourth one is not. Always late for the last element added.

My question is
Screen Shot 2020-07-24 at 8 33 15 pm
should this throw error, the moment i hit append?

i saw you trigger the validation at useEffect

useEffect(() => {
    trigger("items");
}, [fields, trigger]);

It should validate on each change of fields. If you add one, it is valid since there are no 2 jobs with empty name. If you add second empty one, it should validate and mark 2 of them with empty names.

Yea, but the last one you added makes sense not to have error highlight right, cause user hasn't edit it?

step1:
Screen Shot 2020-07-24 at 8 41 09 pm

step2
Screen Shot 2020-07-24 at 8 41 14 pm

there is no right or wrong behavior here, I am just trying to understand what you try to achieve here.

Do you expect step1 to have error?

can you take screenshots and hight when you expected to see errors? It's hard to understand just by words sometimes.

First screenshot shouldn't have error, it is valid because there are no 2 jobs with empty name.

Second screenshot should highlight last 2 jobs because they have empty name = equal name

If you add 3rd job (with empty name), that one should also be highlighted, but it is not.

image

That's driven by your validation logic, right?

you already trigger at useEffect which happens after inputs are mounted. you are getting the latest inputs value as well.

Yes, it is. It saves the indexes of duplicate job names into duplicateNameIndexes Set and this is updated properly, you can check in the console log. But after adding second job it stops re-rendering after validation occurred.

i think perhaps some issues related to your schema?

Screen Shot 2020-07-24 at 9 23 14 pm

No it's not: the screenshot you pasted is valid, there are not 2 jobs with same name.

two jobs with the same name? I am confused: job1, job2, job3 and 332?

I said there are no 2 jobs with same name so it's valid.

if I change that into a pure resolver, as you can see all the values is collected correctly which leads me to think it's a schema issue.

Screen Shot 2020-07-24 at 10 02 38 pm

because errors are really what's returned from the resolver, there is no magic in there.

I understand that resolver returns true/false and that's how it works. It has to populate duplicateNameIndexes Set with indexes so render can highlight fields which are duplicate.

If you look at the console logs you will see that when in case you have 2 jobs without names rendering occurs after validation. When you add third one, validation is triggered, but no render afterwards. Console displays duplicateNameIndexes set that has 3 invalid row indexes, but highlights only 2 since there was no rendering after validation.

ok, let me take a deep looker. thanks

ok, this make sense now. we optimize the same error not triggering extra re-render, I think you should return errors with associated index.

Screen Shot 2020-07-24 at 10 24 11 pm

That's it. Will modify schema validation to return whole path and use that instead of global Set.

Thanks a lot!

It doesn't work: whatever I send through yup's createError method is not reflected in hook-form's errors object:

return createError({
    path: Array.from(duplicateNameIndexes),
    message: message + Date.now()
});

will take look tmr.

Can you take a look at this example with pure resolve return unique errors each time with index?
https://codesandbox.io/s/react-hook-form-usefieldarray-hwgwn?file=/src/index.js

This works, meaning render is triggered after validation. But I would like to use yup since I already have several other validations that work fine. I've tried to change the format in yup validator to match to the one you provided in the example (instead of yup's createError method), but still the same problem.

In lack of better approach, I can solve this by forcing re-rendering after validation like this:

    const [, rerender] = useState(null);
    useEffect(() => {
        // HACK: force re-render after validation
        trigger('items')
            .then(() => rerender(Date.now()));
    }, [fields]);

hmmm, that's not great, I wonder why it's not working. can you capture what are the errors object that you are returning (from yup)? I am busy with stuff today, i am happy to take a look again tmr.

image

(1) If I return only true/false from yup, error stays the same, but if you look at the items you'll see this is not the case (2)
I am logging errors object in console (3) before rendering, so re-rendering is triggered, but duplicate names error is invalid because all items are unique

FYI: I am also logging shape of error that should be returned from yup with createError method (4)

Screen Shot 2020-07-26 at 11 04 32 am

just tried with your CSB, looks like the errors object is always empty.
https://codesandbox.io/s/react-hook-form-usefieldarray-owyo0?file=/src/index.js

I am going to close this issue, but we can continue the conversation here.

It's not: the "validation occured 8 4,3,5,6,7" is log from validation resolver. 4,3,5,6,7 are indexes of the duplicate names (empty) and that is returned from validator. If you look at the console log you will see that this is the last output, there is no rerender afterwards. "duplicate found x" is logged during the render.

What object properties does hook form expect to be returned from validation in order to re-render the form?

have a look at the example here: https://codesandbox.io/s/react-hook-form-usefieldarray-hwgwn?file=/src/index.js at the resolver, as long as the errors object gets returned from resolver is unique, it will trigger re-render, if the errors object is the same, it wouldn't' re-render, we are doing a simple object compare inside.

If you look at both examples that I provided, in case of error, each time different object is created and returned:

https://codesandbox.io/s/react-hook-form-usefieldarray-owyo0?file=/src/index.js:2186-2362
line 99:

      return {
        values: {},
        errors: {
          items: Array.from(duplicateNameIndexes).map(() => ({
            message: "test"
          }))
        }
      };

https://codesandbox.io/s/react-hook-form-usefieldarray-hwgwn?file=/src/index.js:2185-2357
line 98, uses yup's create error:

      return createError({
        path: Array.from(duplicateNameIndexes),
        message: message + Date.now(),
        params: Array.from(duplicateNameIndexes)
      });

The sequence of events is easily tracked via console.log. Afer validation, no re-rendering is taken place.

Here is a third method to validate: inline test function in yup validator:

https://codesandbox.io/s/react-hook-form-usefieldarray-g09z3?file=/src/index.js

it behaves the same, render is after adding 2 items always 1 step behind. You can check this in the console.logs.

And this bug occurs also when you fulfill validation requirements: it doesn't re-render properly and last error is displayed although it is not the case, see this screenshot:

image

you have 3 different names, so this is valid, but it didn't re-rendered the form. Last code logged in console is that validation is passed, but nothing updated on the form.

Screen Shot 2020-07-27 at 8 15 12 pm

but look at the errors object its always the same shape as the same message and type. the moment we doing a diff inside it's the same, hence not re-render the component.

I understand that - and what I am trying to find out here is how to make this error object different if I can return from yup true or false? How do I modify what gets into rhf's error object? I've already tried true/false, and createError and shape you mentioned before.

And please try the last example I've posted: if you make the form valid, last error is not cleared from the errors object.

i think it's almost easier to just use resolver and have scheme inside, so you can write some custom logic.

resolver() {
  // yup()
  // some custom logic
}

Here is the version with no duplicate error types, each has a hash for different naming, still same behaviour:

https://codesandbox.io/s/react-hook-form-usefieldarray-ziz2o?file=/src/index.js

i think it's almost easier to just use resolver and have scheme inside, so you can write some custom logic.

resolver() {
  // yup()
  // some custom logic
}

Here is the version with resolver you've suggested:

https://codesandbox.io/s/react-hook-form-usefieldarray-9io4r?file=/src/index.js

still same behavior: rendering is 1 step behind validation.

I also checked with "react-hook-form": "^5.7.2", the behavior is the same:

  • If you add third empty job, it will not be highlighted.

  • if you fulfill the validation, last error message is not cleared; It still shows that there are duplicates, but there aren't any.

I think the render is correct with your latest codesandbox, but why you are not using the errors object? if your red board can rely on the errors object, then the UI should render correctly.

if your red board can rely on the errors object, then the UI should render correctly.

That's what I am trying to do but with no luck :)

  • if I return true from yup validation, error is the same and no-rerendering
  • if I return object created with createError, the same
  • if I return something in the shape of errors object, the same

or you mean I should manipulate errors object directly?

yes manipulate the errors object, that's what i meant from the resolver.

It's not working properly, look at the console, validation always happens last, no rerendering afterwards:

https://codesandbox.io/s/react-hook-form-usefieldarray-9io4r?file=/src/index.js

Screen Shot 2020-07-28 at 7 25 03 pm

is it this looks correct? two fields two errors? unless I am completely miss something.

wait look at the errors object still empty :*(

yes, it should be 2 errors

but errors object is empty because this was previous render, it is logged while rendering, that should happen AFTER valdiation

what about this approach?

useEffect(() => {
  trigger('xxx');
}, [trigger, fields])
  1. resolver returned errors are not what you after
  2. trigger extra validation after fields updated with trigger

if above both can't resolve your problem, then I have no clue or better way to resolve your problem.

I think the problem is this: (yup resolver):

https://codesandbox.io/s/react-hook-form-usefieldarray-g09z3?file=/src/index.js

  • if you're validating single fields in array, errors will get them and display properly because there should be indexes for each of the fields (codesandbox line 101)
  • if you're validating array (codesandbox line 99 and 108), in this case error is binded to array and not dynamic (freshly created) list of indexes for the items in the array. Therefore, error is always the same and no re-rendering occurs. This behavior can be seen if you add 3 jobs one after another. For first 2 it will display error and last one won't trigger re-rendering, because error is the same as it applies to whole array.

If possible, I will try now to rewrite validation to back-reference whole array while checking one of the items, and I think that should work. It will probably be an overkill, but will return new indexes each time.

  • if you're validating array (codesandbox line 99 and 108), in this case error is binded to array and not dynamic (freshly created) list of indexes for the items in the array. Therefore, error is always the same and no re-rendering occurs. This behavior can be seen if you add 3 jobs one after another. For first 2 it will display error and last one won't trigger re-rendering, because error is the same as it applies to whole array.

That's what i try to suggest to kick another trigger validation after at the useEffect

  • if you're validating array (codesandbox line 99 and 108), in this case error is binded to array and not dynamic (freshly created) list of indexes for the items in the array. Therefore, error is always the same and no re-rendering occurs. This behavior can be seen if you add 3 jobs one after another. For first 2 it will display error and last one won't trigger re-rendering, because error is the same as it applies to whole array.

That's what i try to suggest to kick another trigger validation after at the useEffect

if you mean this:

useEffect(() => {
  trigger('xxx');
}, [trigger, fields])

that's not working

I've managed to make it work. It's overkill, but I don't know any other way:

https://codesandbox.io/s/react-hook-form-usefieldarray-4pzic?file=/src/index.js

line 101: it tests for each job object the whole array of jobs. Errors object is populated as it should.

But if you add 2-3 empty jobs and start typing name in one of them, it doesn't revalidate

adding

onChange={() => trigger("items") }

on input name solves this problem, but then I don't know why is this not working:

mode: "onChange",
reValidateMode: "onChange",

mode: 'onChange' is validating at the input level, when you trigger by key at array field level.

Well, it seems impossible with the yup to achieve that. If I put the test function for duplicate names for the "customJobName" field, there is no way to back-reference the array of jobs, only the containing object, which is the job (item) of the fieldArray. This is obviously needed for checking the duplicates.

I think the whole misunderstanding about the validation was that I considered duplicate names a validation of the whole jobs array (fieldsArray), but instead it works by validating each job (item) separately. I still think this is an overkill, since it has to go through whole array for each of the jobs instead of resolving duplicates in one validating function.

I will go with https://codesandbox.io/s/react-hook-form-usefieldarray-4pzic?file=/src/index.js and

onChange={() => trigger("items") }

for input fields - this works fine.

Bill, thanks for your patience and great library 馃憤

Awesome @rvision I am glad at least you found out something that works for you. by the way, it's my pleasure to help, I mean the whole point of making this lib and maintaining the project it to help everyone around the world. 鉂わ笍

Was this page helpful?
5 / 5 - 1 ratings