-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
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
Comments
Copying over comments from the other couple threads for reference:
|
|
#1248 (comment) (@markerikson ):
|
|
As I understand the main reason to have the object form of 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. |
Moving my comment #1248 (comment)
|
Moving my comment #1248 (comment)
|
@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. |
In 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 const selTodoItem = ([id]) => state => state.todos[id]; Would turn into: const selTodoItem = 'todos.%0'; |
@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. |
@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 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. |
@Satyam : I'm going to have to disagree with several of your opinions here:
|
@Satyam the 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)) |
The reason for
The string interpolation was just a wish list item of mine, I shouldn't have mentioned it along the main issue, sorry.
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
Actually, when I wrote that part, the documented API didn't have the @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 |
@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 |
@MrWolfZ these look amazing! Nice job! Here are my two cents:
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 react-redux/src/hooks/useSelector.js Line 91 in 87841fa
I wonder if this would be better as an 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 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 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 function Container() {
return <PresentationalComponent addTodo={useAction(addTodo)} />
} However, there is another idea I've had inspired by In material-ui/styles, they have a factory 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 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. |
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 Finally, about the "controversy" on whether Thanks again for this! |
I'm not sure I understand. Yes, a new result for
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
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
Yeah, I thought about this as well. First of all, even with the current implementation nothing keeps you from calling
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 const makeAction = ac => () => useActions(ac)
I have to admit I am not very familiar with const getUsers = state => state.users
const getUser = id => createSelector(
getUsers,
users => users[id]
)
const user = useSelector(getUser(props.id), [props.id]) In that example, |
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. |
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)
},
[]
)
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. |
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 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! 👍 |
@janhesters : yep, good catch. If you've got time, please submit a PR to fix those examples in the docs on @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! |
@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
}) |
I'd say change |
But I can't find Do you mean the |
Correct - due to the docs setup, the docs themselves are on |
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 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... |
@josepot : no. If you look way back in the previous hooks thread, I originally proposed adding a It's a tradeoff - simplicity vs customization. |
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. |
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? |
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? 😃 |
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 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? |
@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. |
@lrdxbe if you want you can still create a "connected" version of 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 |
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. |
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 |
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 But I, too, am definitely interested in learning more about react-testing-library and its more integration focused approach! On my to-learn list 😄 |
Please use this thread to give feedback on the new React-Redux hooks APIs available in 7.1-alpha.
Prior references and discussion:
The text was updated successfully, but these errors were encountered: