Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feedback: React-Redux v7 hooks alpha #1252

Closed
markerikson opened this issue Apr 22, 2019 · 244 comments
Closed

Feedback: React-Redux v7 hooks alpha #1252

markerikson opened this issue Apr 22, 2019 · 244 comments

Comments

@markerikson
Copy link
Contributor

markerikson commented Apr 22, 2019

Please use this thread to give feedback on the new React-Redux hooks APIs available in 7.1-alpha.

Prior references and discussion:

@markerikson
Copy link
Contributor Author

Copying over comments from the other couple threads for reference:

#1179 (comment) (@mungojam ):

Very cool to see this coming into shape.

The new docs don't look quite right for the useCallback with useSelector example (though useCallback vs. useMemo isn't fully in my head yet):

const todoSelector = useCallback(() => {
    return state => state.todos[props.id]
}, [props.id])

const todo = useSelector(todoSelector)

It seems like the useCallback is actually being used as if it were useMemo, when it should just be:

const todoSelector = useCallback(
  state => state.todos[props.id], 
  [props.id]
)

const todo = useSelector(todoSelector)

The equivalent useMemo version would be as per the current example:

const todoSelector = useMemo(() => {
    return state => state.todos[props.id]
}, [props.id])

const todo = useSelector(todoSelector)

@markerikson
Copy link
Contributor Author

#1179 (comment) (@mungojam ):

Sorry if I've missed the answer to this earlier, but what is the idea behind having the dependencies array in useAction, but not in useSelector? It would be nice if useSelector had it too as that feels more hooks-idiomatic and we wouldn't then need to use useMemo or useCallback if we include props in it.

@markerikson
Copy link
Contributor Author

#1179 (comment) (@MrWolfZ ):

@mungojam You didn't miss anything. It was indeed an oversight on my part that I noticed myself over the weekend, as discussed in this post. I think it makes perfect sense to add the deps to useSelector, but it might make useRedux a bit more tricky.

@markerikson
Copy link
Contributor Author

#1248 (comment) (@markerikson ):

A couple quick observations as I work on docs:

  • useRedux() doesn't allow you to pass a deps array for actions

  • We probably oughta tweak the error message for useReduxContext() a bit

  • Do we need that same "didUnsubscribe" check in useSelector() that we have in connect()?

  • Do we want to make an empty deps array the default for useActions()? How often would you actually want to recreate those every time?

@markerikson
Copy link
Contributor Author

#1248 (comment) (@MrWolfZ ):

@markerikson I worked a bit with the hooks on the weekend, and also made an observation. Currently, the selector in useSelector is re-created every time. That means if you use a memoizing selector you need to memoize the selector itself yourself. This could be fixed by also passing a deps array into useSelector.

Now my comments to your observations:

useRedux() doesn't allow you to pass a deps array for actions

Indeed, I completely missed this. It would be easy enough to add this parameter, but if we decide to add deps to useSelector as mentioned above, then things get messy. Do we only use a single deps for useRedux? Or pass in two of them? I think this is an argument for removing this hook after all.

Do we need that same "didUnsubscribe" check in useSelector() that we have in connect()?

I intentionally removed this, since it was only required to ensure the selector never sees inconsistent state and props. Basically, with my implementation it does not matter whether the subscription callback is called after the component was unmounted. The only thing that happens is potentially a forceRender call that does nothing. Also, based on this issue the didUnsubscribe guard doesn't seem to be enough to prevent the callback to be called after unmounting anyways. On the flip side it doesn't hurt to have that check in there and it could give minimal performance improvements by not calling the selector when we know we don't need to. I prefer having a simpler implementation (and not having to write a test for it ;) ), but feel free to add it back in.

Do we want to make an empty deps array the default for useActions()? How often would you actually want to recreate those every time?

I strongly argue to not make an empty deps array the default. Firstly, to be consistent with how useEffect, useMemo, and useCallback work. Secondly, if we were to do this, it might lead to very subtle bugs with stale props inside the actions that are incredibly hard to debug. By leaving the default undefined the worst thing that can happen is slightly worse performance and that can easily be fixed by just adding the deps yourself.

@vadzim
Copy link

vadzim commented Apr 22, 2019

As I understand the main reason to have the object form of useActions is to use all the actions from a module like

import * as actions from 'myactions'
...
const { action1, action2 } = useActions(actions)
...

I see a possible disadvantage here that statically analyzing tools will not be able to understand that some of actions are not used in a project anymore. E.g. WebStorm notifies about unused exports in a project.

@mungojam
Copy link

Moving my comment #1248 (comment)

const boundAC = useActions(actionCreator : Function, deps : any[])

const boundACsObject = useActions(actionCreators : Object<string, Function>, deps : any[])

const boundACsArray = useActions(actionCreators : Function[], deps : any[])

Unless there is a compelling reason for both, I think it would be good to drop either the 2nd or 3rd version of useActions. I'd rather have just one way to create multiple actionCreators.

I probably prefer the object one as it is more auto-documenting within the hook. The hooks that use arrays like useState etc. are different because it is for a single thing and the two array elements usually have a single name i.e. [count, setCount]. It also lets users name them easily rather than {state: count, setState: setCount}. You don't have that problem with useActions.

@mungojam
Copy link

Moving my comment #1248 (comment)

Do we only use a single deps for useRedux? Or pass in two of them? I think this is an argument for removing this hook after all.

Personally I think useRedux isn't very hooksy. Either people will write their action creator and selector logic within the useRedux call and then it's quite verbose, or they will write them outside and then what is actually being usefully added by useRedux? Maybe there are some more advanced scenarios it could cater for if added later but I don't think it should be added if it's just for familiarity with connect as it's just an added layer over the other hooks.

@threehams
Copy link

@MrWolfZ Are you planning to maintain a PR at https://github.com/DefinitelyTyped/DefinitelyTyped? If not, I can add one later tonight and maintain it with alpha changes.

@Satyam
Copy link

Satyam commented Apr 22, 2019

In useSelector, in line 55, where it says:

const memoizedSelector = useMemo(() => selector, deps)

Wouldn't it make sense to have:

const memoizedSelector = useMemo(() => () => selector(deps) , deps)

so you could do:

const todo = useSelector(([id]) => state => state.todos[id], [props.id])

I would also love to have the option of using a path for a selector, much like lodash _.get() but using placeholders for dependencies, thus:

const selTodoItem = ([id]) => state => state.todos[id];

Would turn into:

const selTodoItem = 'todos.%0';

@markerikson markerikson pinned this issue Apr 22, 2019
@MrWolfZ
Copy link
Contributor

MrWolfZ commented Apr 22, 2019

@threehams thanks for reminding me. I have created a PR with the hooks types. Any feedback is highly welcome, since it is the first time I am contributing to DefinitelyTyped.

@Satyam what is the benefit of getting the props into the selector as an argument instead of just by closure? One downside would definitely be more typing. Regarding the path, I feel that is a very special case that you can easily create a custom hook for. One of the points of hooks was to make it easy to compose them and to make it easy to create your own. I am not the maintainer of this repo, so I don't have the last say on this, but I definitely feel we should leave the hooks as simple as possible and not overload them too much.

@Satyam
Copy link

Satyam commented Apr 22, 2019

@MrWolfZ The reason for having the selectors get the props, or whatever other dependency, as an argument is that I don't usually mix details of the redux store along the components. I have the selectors defined somewhere along the store so that if the store changes in any way, I don't have to go searching for all the affected selectors embedded within the components. Likewise, if the same selector is used in several places, I don't have to copy and paste the same selector code into the several components that use it. And it is not the component props that I mean to use as arguments for the selector but any value taken from wherever it might be. The arguments used by the selector and its dependencies are one and the same, I can hardly imagine a selector that has more dependencies than arguments or vice versa.

Also, I don't see why those arguments are to be passed as an array of dependencies, it might be better to have it as an object so the arguments can be named. The fact that they are also dependencies in the memoized selector should be an implementation detail solved internally, for example:

const memoizedSelector = useMemo(() => () => selector(deps) , Object.values(deps))

So, the selector could be called:

const todo = const todo = useSelector(selTodoItem, { id: props.id});

where selTodoItem would be defined elsewhere as :

const selTodoItem = ({id}) => state => state.todos[id];

which clearly is not such an improvement over my previous example with just one argument passed as an array, but it would make things clearer if there were more than one argument.
So, basically, the principle of separation of concerns is what makes it funny to have the action creators and the reducers all close to the store, but the selectors along the components, just to leverage the closure.

@markerikson
Copy link
Contributor Author

@Satyam : I'm going to have to disagree with several of your opinions here:

  • I don't understand your proposal for const todo = useSelector(([id]) => state => state.todos[id], [props.id]) at all
  • We're definitely not going to add some kind of a string interpolation overload. If you want to generate selectors along that line, it should be easy enough to do yourself and pass them to useSelector
  • Dependency arrays are the natural form here because that's how useMemo and useCallback work. There's no reason to ask users to define them as an object, only to promptly convert the values back into an array.
  • If you want to pre-memoize the selector function yourself, you can do that. But, given the common case of selecting values based on props, it's reasonable for us to offer an API that just handles that automatically if the deps are supplied.

@MrWolfZ
Copy link
Contributor

MrWolfZ commented Apr 23, 2019

@Satyam the Object.values approach won't work, since it uses the same order as for...in and that order is arbitrary and not necessarily stable, as you can see here.

Regarding passing args to the selector, I assume the most common use case will be inline selectors that can just use closure. However, even for your case you can just do this:

const todo = useSelector(selTodoItem({ id: props.id}), [props.id]);

If you really want to have such a hook (and don't care about stability of props order), it is easy to implement it:

const useMySelector = (selector, deps) => useSelector(selector(deps), Object.values(deps))

@Satyam
Copy link

Satyam commented Apr 23, 2019

@markerikson

@Satyam : I'm going to have to disagree with several of your opinions here:

  • I don't understand your proposal for const todo = useSelector(([id]) => state => state.todos[id], [props.id]) at all

The reason for const todo = useSelector(([id]) => state => state.todos[id], [props.id]) is that the selector can be defined elsewhere, more likely along the store itself, like shown later: const selTodoItem = ([id]) => state => state.todos[id];. As the argument id comes as an argument, it doesn't rely on closure which requires theselector to be defined within the component.

  • We're definitely not going to add some kind of a string interpolation overload. If you want to generate selectors along that line, it should be easy enough to do yourself and pass them to useSelector

The string interpolation was just a wish list item of mine, I shouldn't have mentioned it along the main issue, sorry.

  • Dependency arrays are the natural form here because that's how useMemo and useCallback work. There's no reason to ask users to define them as an object, only to promptly convert the values back into an array.

It feels like the external API is being defined by the internal implementation of the API and not by the way a developer might use it. The developer should not be concerned if the API uses useMemo or whatever else internally. It should not be defined because useMemo expects dependencies, it should be defined because the selector needs arguments which happen to be its dependencies.

  • If you want to pre-memoize the selector function yourself, you can do that. But, given the common case of selecting values based on props, it's reasonable for us to offer an API that just handles that automatically if the deps are supplied.

Actually, when I wrote that part, the documented API didn't have the deps argument yet so it no longer applies.

@MrWolfZ That is true, redundant as it is. If the deps are the arguments to the selector, why not giving them to it? I don't know the reason why useMemo doesn't provide the memoized function with the arguments but it clearly is something a developer would expect. The way the issue is highlighted in the documentation clearly shows that it is contrary to normal expectation. I would assume that there is some technical reason for that in useMemo, I don't see any reason not to have it in useSelector and avoid the unnecessary repetition or tying up the selector to the component by closure.

@Satyam
Copy link

Satyam commented Apr 23, 2019

@MrWolfZ Besides, if you do it the way you suggest:

const useMySelector = (selector, deps) => useSelector(selector(deps), Object.values(deps))

What is the point of memoizing if the part that does depend on the arguments, that is selector(deps) has already been evaluated?
Anyway, my point in passing arguments as an object was to show that there are many ways to define the API for useSelector that are better oriented to the developer using it and are not so much focused on the way useMemo works.

@ricokahler
Copy link
Contributor

@MrWolfZ these look amazing! Nice job!

Here are my two cents:

I worked a bit with the hooks on the weekend, and also made an observation. Currently, the selector in useSelector is re-created every time. That means if you use a memoizing selector you need to memoize the selector itself yourself. This could be fixed by also passing a deps array into useSelector.

I think the deps array should be a required argument, it makes things faster and isn't much of a mental lift for the user to add. The current typings for useMemo also require the deps array so I think it wouldn't be a bad idea to make that argument required and invariant if it's not there.


if (shallowEqual(newSelectedState, latestSelectedState.current)) {

I wonder if this would be better as an Object.is or === check here. I've messed with some redux hooks and I've always used multiple useSelector calls vs returning an object e.g.

function Foo() {
  // returning an object where shallowEqual helps
  const { a, b } = useSelector(state => ({ state.a, state.b }), []);

  // using multiple calls to `useSelector`
  const a = useSelector(state => state.a, []);
  const b = useSelector(state => state.b, []);
  // i like doing this 👆better
}

I like this better because it feels simpler. If you're returning part of the store without any calculations or creation of objects/arrays, you don't need to shallowEqual. It also feels more hook-esque to call useSelector more than once.

In cases where I would want to create an object within the selector, I like to pass in a memoized function e.g.

import { createSelector } from 'reselect';

const selectA = state => state.a;
const selectB = state => state.b;
const selectAB = createSelector(selectA, selectB, (a, b) => ({ a, b }));

function Foo() {
  const ab = useSelector(selectAB, []);
}

and then memoized function would return the same object reference not needing the shallow equal check either.

Maybe that's more complicated? but it fits in with the current Redux ecosystem so I'm okay with it.


In regards to Action Object Hoisting, I've actually found it to be relatively ergonomic to just useDispatch and wrap all action calls in dispatch.

function Foo(personId) {
  const dispatch = useDispatch();
  
  useEffect(() => {
    dispatch(fetchPerson(personId));
  }, [personId]);

  // ...
}

The only time where I found it to be more ergonomic to use something like useActions is when passing abound version of an action creator to a lower components. I've done it like this:

function Container() {
  return <PresentationalComponent addTodo={useAction(addTodo)} />
}

However, there is another idea I've had inspired by @material-ui/styles:

In material-ui/styles, they have a factory makeStyles that returns a hook useStyles

import React from 'react';
import { makeStyles } from '@material-ui/styles';

const useStyles = makeStyles({
  root: {
    backgroundColor: 'red',
  },
});

export default function MyComponent() {
  const classes = useStyles();
  return <div className={classes.root} />;
}

Similarly, we could create a factory makeAction that will wrap an action creator and return a hook:

import React from 'react';
import { makeAction } from 'react-redux';
//                               👇 could also pass in an existing action creator
const useAddTodo = makeAction(todoName => ({ type: 'ADD_TODO', payload: todoName }));

function Foo() {
  const addTodo = useAddTodo();

  // ...
}

Maybe that's more verbose but I like the idea and conventions of it. Let me know what you think.

@josepot
Copy link
Contributor

josepot commented Apr 23, 2019

Hi and thanks a lot for this @MrWolfZ and @markerikson !

I like it, a lot!

Although, I agree with @ricokahler in that I would prefer for useSelector to perform an Object.is or a === instead of a shallow-compare. I understand that the first thing that the shallowCompare does is to check for referential equality, and that the only difference that it would make for me would be the few unnecessary comparisons that will take place when the result of the selector has actually changed... But still, I would much rather if useSelect didn't perform a shallow-compare by default... Perhaps that could be an option? or another hook useObjectSelector?

Finally, about the "controversy" on whether useSelector should accept props or not. I like the fact that it doesn't. However, if Reselect v5 proposal got accepted, perhaps it would make sense to include a different hook named useInstanceSelector (or something like that) that could accept props and "usable selectors". Although, I understand that could be seen as coupling this API to the API of an optional dependency, so I can see how that could be controversial... Although, I still think that it would be an idea worth considering.

Thanks again for this!

@MrWolfZ
Copy link
Contributor

MrWolfZ commented Apr 23, 2019

@Satyam

@MrWolfZ Besides, if you do it the way you suggest:

const useMySelector = (selector, deps) => useSelector(selector(deps), Object.values(deps))

What is the point of memoizing if the part that does depend on the arguments, that is selector(deps) has already been evaluated?

I'm not sure I understand. Yes, a new result for selector(deps) is always created, but the callback that is memoized and then used to select from the state is the first one that was created for each deps. So if selector memoizes on either the deps, the state or both it will work.

Anyway, my point in passing arguments as an object was to show that there are many ways to define the API for useSelector that are better oriented to the developer using it and are not so much focused on the way useMemo works.

I'm just gonna say that your suggestion is oriented towards how you are using the API, but I am sure there are loads of other ways to use it we can't even think of. The idea behind making it work the same as useMemo and useCallback is to a) make it immediately familiar to all react hooks users, and b) to build upon the foundations that the react team built that they surely have put some thought into. Also, with your suggestion, the semantics of the first parameter would change based on whether the second parameter was passed (i.e. we would probably need different handling for no deps, empty deps, and filled deps). This makes the API very complex and also makes it difficult to add the deps parameter later on. I completely understand where you are coming from with this proposal, but in the end, there is no right answer to API design and this mostly comes down to personal preference, and I prefer to stick with the API I implemented.

@ricokahler

I think the deps array should be a required argument, it makes things faster and isn't much of a mental lift for the user to add. The current typings for useMemo also require the deps array so I think it wouldn't be a bad idea to make that argument required and invariant if it's not there.

I don't mind making it mandatory, but I also don't strongly feel that it needs to be. The only downside of not providing it would be potentially worse performance and the fix to that is easy. I also think this case is slightly different to useMemo in that useMemo would be completely pointless without the deps but useSelector works just fine without it.

I wonder if this would be better as an Object.is or === check here.

Yeah, I thought about this as well. First of all, even with the current implementation nothing keeps you from calling useSelector multiple times. Secondly, the downside of not doing shallowEquals is that useSelector(state => ({ a: state.a, b: state.b })) is now always causing a render and the fix to that would be not very obvious. I think that usage pattern will be common enough to justify doing shallowEquals. Lastly, I have actually benchmarked the hooks both with reference equality and shallowEquals and the results were almost identical, even though the benchmarks exactly trigger the sub-optimal behaviour by selecting large arrays from the state where each value is compared with shallowEquals (sadly I didn't keep the results around but you can easily reproduce this if you want).

Similarly, we could create a factory makeAction that will wrap an action creator and return a hook:

Yup, I have thought about this as well and I am actually doing something similar in a (toy) project I work on. That said, the current API does in fact already allow you to do this easily. Just do this:

const useAddTodo = () => useActions(todoName => ({ type: 'ADD_TODO', payload: todoName }))

If you find that unwieldy you can always easily create makeAction yourself:

const makeAction = ac => () => useActions(ac)

@josepot

Finally, about the "controversy" on whether useSelector should accept props or not. I like the fact that it doesn't. However, if Reselect v5 proposal got accepted, perhaps it would make sense to include a different hook named useInstanceSelector (or something like that) that could accept props and "usable selectors". Although, I understand that could be seen as coupling this API to the API of an optional dependency, so I can see how that could be controversial... Although, I still think that it would be an idea worth considering.

I have to admit I am not very familiar with reselect and the likes. However, all the examples I see at reselect and also in redux-views have selectors that depend on how mapStateToProps works, i.e. taking state and props as arguments. With hooks you can actually achieve the same by just using closure (basically, by doing what I suggested above already):

const getUsers = state => state.users

const getUser = id => createSelector(
  getUsers,
  users => users[id]
)

const user = useSelector(getUser(props.id), [props.id])

In that example, useSelector takes care of memoizing on the props, while reselect takes care of memoizing on the users. However, as I said I am not so familiar with this approach, so please let me know if I made a mistake in my way of thinking.

@markerikson
Copy link
Contributor Author

FWIW, it's always possible for us to add more hooks down the road, so if anything I would prefer to keep the initial list relatively minimal and primitive. On the flip side, I also don't want to have to go changing the hooks we ship (no more major versions!), so we want to get these initial APIs correct before they go final.

@janhesters
Copy link
Contributor

janhesters commented Apr 23, 2019

Don't know if this is the right place for mistakes in the docs and whether this is indeed an error, but in the next docs it says:

const increaseCounter = ({ amount }) => ({
  type: 'increase-counter',
  amount
})

export const CounterComponent = ({ value }) => {
  // supports passing an object of action creators
  const { increaseCounterByOne, increaseCounterByTwo } = useActions(
    {
      increaseCounterByOne: () => increaseCounter(1),
      increaseCounterByTwo: () => increaseCounter(2)
    },
    []
  )

increaseCounter(1) would throw, because 1 has no property called amount

I think this should be:

increaseCounterByOne: () => increaseCounter({ amount: 1 }),
increaseCounterByTwo: () => increaseCounter({ amount: 2 })

or

const increaseCounter = amount => ({
  type: 'increase-counter',
  amount
})

I might be wrong here though.

@G710
Copy link

G710 commented Apr 23, 2019

Hi,

first of all, thank you for your hard work and thoughts that went into this library (and this new API in particular 👍 )

I've been experimenting with the lib today and so far I really like it! The provided hooks fit perfectly into the new eco-system and feel natural to use. I've never really warmed up to the mapXToProps functions but the hooks just make sense to me.

I don't really get the fuzz about the useSelector deps. I mean, you can pass props to the function in it's current state, right? (seems to be working in my project) You just have to be careful that your selector can't run into an invalid state.

As far as I understand most suggestions so far can be build upon the existing functions and are easy enough to a) just implement in the project or b) write up a small lib.

Let's see if I run into any issues in the next few days but so far the transition seems to be seamless. Thanks a lot! 👍

@markerikson
Copy link
Contributor Author

markerikson commented Apr 23, 2019

@janhesters : yep, good catch. If you've got time, please submit a PR to fix those examples in the docs on master and in the source comments in v7-hooks-alpha (which is where I copied them from).

@G710 : yes, you can totally use props in selectors, it's just that there's certain cases you have to watch out for. Thanks for the feedback!

@janhesters
Copy link
Contributor

janhesters commented Apr 23, 2019

@markerikson Sure. Which of the two fixes do you want me to create a PR for?

A:

increaseCounterByOne: () => increaseCounter({ amount: 1 }),
increaseCounterByTwo: () => increaseCounter({ amount: 2 })

or B:

const increaseCounter = amount => ({
  type: 'increase-counter',
  amount
})

@markerikson
Copy link
Contributor Author

I'd say change amount from a destructured object to a simple number, to match the other usages (increaseCounter(2)).

@janhesters
Copy link
Contributor

PR for master.

But I can't find hooks.md in the v7-hooks-alpha branch 🤔

Do you mean the hooks-docs branch?

@markerikson
Copy link
Contributor Author

Correct - due to the docs setup, the docs themselves are on master, even though we've got the code over in that v7-hooks-alpha branch.

@alexej-d
Copy link

alexej-d commented Jun 4, 2019

Please add a Context parameter to hooks API: useSelector, useDispatch, etc...

@MGaburak-eleks why? What's the use case?

I think I know what is meant. I had a suiting use case. Imagine different nested stores with the same keys and child components listening and changing the same keys but on different nesting levels. Sometimes you do not want to have one single store but context dependent.

@josepot
Copy link
Contributor

josepot commented Jun 4, 2019

I think I know what is meant. I had a suiting use case. Imagine different nested stores with the same keys and child components listening and changing the same keys but on different nesting levels. Sometimes you do not want to have one single store but context dependent

Can't you do that by using different redux Providers?

Although, in all honesty, if you are actually using different stores for different "contexts", I wonder whether Redux is the right tool for what you are doing...

@markerikson
Copy link
Contributor Author

@josepot : no. If you look way back in the previous hooks thread, I originally proposed adding a customContext parameter to the hooks to allow this customization, and the consensus was that wasn't worth including. So, our useStore() hook directly pulls in the singleton ReactReduxContext instance and reads the store from that, and all the other hooks rely on useStore(). If you do supply a custom context to <Provider>, the hooks currently have no way of accessing the store from that context instance.

It's a tradeoff - simplicity vs customization.

@MGaburak-eleks
Copy link

On the documentation page https://react-redux.js.org/next/api/connect#options-object connect support context parameter I think hooks also should support this feature.

@markerikson
Copy link
Contributor Author

I don't think we're going to add that for the initial release.

If there's sufficient demand for it, we may consider it down the road.

@MGaburak-eleks : what is your specific use case for needing a custom context parameter?

@leoyli
Copy link

leoyli commented Jun 4, 2019

@markerikson, @timdorr,

Thank you for these amazing work ❤️

Since RC.1 was out almost a week and look like there wasn't any blocker, do you guys have a targeting date for the final release? 😃

@lrdxbe
Copy link

lrdxbe commented Jun 5, 2019

I would really love to make the switch but right now I'm still struggling figuring out how my test will be made. I'm using TypeScript and react-testing-libary. Right now testing connected components is easy: I export the "raw" component and test it instead of the connected one.

type Todo = {
  id: number;
  text: string;
};

type AppState = {
  todos: {
    list: Todo[]
  }
}

type TodosProps = {
  addTodo: (todo: Todo) => void;
  todos: Todo[];
};

const TodosComponent: FC<TodosProps> = ({ todos, addTodo }) => {
  const list = todos.map(todo => <p key={todo.id}>{todo.text}</p>);
  const randomTodo: Todo = {
    id: Math.ceil(Math.random() * 1000),
    text: "A new one"
  };
  return (
    <>
      <p>
        <button type="button" onClick={() => addTodo(randomTodo)}>
          Add a todo
        </button>
      </p>
      <h3>List</h3>
      {list}
    </>
  );
};

const mapStateToProps = (state: AppState) => ({
  todos: state.todos.list,
});

const mapDispatchToProps = {
  addTodo: addTodoActionCreator,
};

const Todos = connect(mapStateToProps, mapDispatchToProps);

Here I could easily test TodosComponent and pass it jest.fn() for addTodo (dispatch props) and an array of my choice for its todos (state props). I don't need to create a store with a correctly typed state just to test this component.

But with hooks, I cannot exract redux from my component. Which means that I need to create a store with a correctly typed state (optionnaly hydrated with the values I want). This doesn't seem convenient and make my tests consume way more external dependencies (redux, action creators, selectors) which isn't arguably that bad (since react testing is almost always integration testing anyway).

I tried to found resources on the matter but couldn't, I guess it's normal since the use of hooks in Redux is not even officially release yet. But I wonder if you guys have any leads on this?

@markerikson
Copy link
Contributor Author

@lrdxbe : yeah, as @timdorr just said over in #1001 , it would be great if we could get some docs on testing added.

That said, I personally don't have the experience (or time) to write those. I'd really appreciate it if someone else could come up with some good strategies and file a PR documenting those.

@lukaszfiszer
Copy link

lukaszfiszer commented Jun 5, 2019

@lrdxbe if you want you can still create a "connected" version of TodosComponent using hooks:

const Todos = () => {
  const todos = useSelector(state => state.todos.list);
  const dispatch = useDispatch();
  const addTodo = useCallback((todo) => dispatch(addTodoAction), [dispatch]);
  return <TodosComponent todos={todos} addTodo={addTodo} />
}

However I'd recommend against using that. Testing TodosComponent in isolation from Redux store does not give you any confidence that the component really works in the context of your application. You should test Redux integration on some level, and integration testing with RTL seems like a ideal place to do it.

@lrdxbe
Copy link

lrdxbe commented Jun 6, 2019

I don't really see the point in using a container component just to do that either. Like Kent C. Dodds said, this Container/Presenter pattern doesn't make a lot of sense in the age of hooks.

I could (and maybe will be forced to do that) test my connected components with all the store/reducers/action creators/selectors logic but that would be tedious and I've already got dedicated tests for those. Testing everything together is already been made with e2e tests. While I get that react testing is mostly integration testing (I'm embracing the react-testing-library philosophy), I still feel like some level of isolation would be needed for UI testing.

@SeanRoberts
Copy link

Hey that was my AMA question for Kent... I'm on TV!

For testing redux connected components I use my app's custom test render function that sets up all my necessary providers and takes options to hydrate each of them. I'm also using Typescript and also didn't want to have to deal with passing a fully hydrated store just to test some tiny slice of it in relative isolation so in my test render function I just typed it as any. Having a fully typed store is great when I'm running my full app because it saves me from making big mistakes but I don't mind using the any escape hatch in my integration tests because if I make a mistake my test is going to fail anyway. Once I hydrate the initial state that a component needs all the action creators/selectors/etc. just work.

@Joonpark13
Copy link

Joonpark13 commented Jun 17, 2019

Sorry to chime in late, but I wanted to post a note for @mdcone or anyone else who might be coming to this thread for questions with usage with enzyme, as I just did: I've found that if you spy on useSelector and useDispatch, the provider issues go away. I understand that some may argue this isn't great practice, but it's obvious that there are at least a handful of us that are out there who are bound (at least for the time being) to large codebases at work that would be very difficult to migrate testing libraries, and for me this approach has worked fine (and I find myself needing to assert on spy calls anyway).

But I, too, am definitely interested in learning more about react-testing-library and its more integration focused approach! On my to-learn list 😄

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests