Can you please explain why await on dispatch has no effect?.Use dispatch is synchronous by default. But is there any way to make it to async?
I have one issue by using dispatch and createAsyncthunk.I think it halts the render of other components. I may be wrong please suggest a better way to handle this rendering issue. I think dispatch is still synchronous.
//API services
const getPersonLists = async (query) => {
return await axios.get(`${endPoint}/person?page=${query.page}&perPage=${query.perPage}`);
};
const fetchPeronWithAsyncThunk = createAsyncThunk('userSlice/userList', async (query) => await getPersonLists(query));
//Slice
const userSlice = createSlice({
name: 'userSlice',
initialState: {
users: [],
loading: false,
},
extraReducers: {
[fetchPeronWithAsyncThunk.pending]: (state) => {
state.loading = true;
},
[fetchPeronWithAsyncThunk.rejected]: (state) => {
state.loading = false;
},
[fetchPeronWithAsyncThunk.fulfilled]: (state, action) => {
state.loading = false;
state.users = action.payload;
},
},
});
//Component
const MyComponent = () => {
const { users } = useUserList(); //selector
const dispatch = useDispatch();
const getList = async () => {
//await has no effect
await dispatch(fetchPeronWithAsyncThunk({ page: 1, perPage: 10 }));
};
return (
<div>
<button onClick={getList}>Fetch user</button>
<div>{users.length && users.map((user, index) => <div key={index}>{user?.name}</div>)}</div>
</div>
);
};
await at that point has exactly the effect you are waiting for.
But since you are in a closure your state will not update within your getList function.
You can get the result of the thunk in your code though:
const result = await dispatch(fetchPeronWithAsyncThunk({ page: 1, perPage: 10 })).unwrap();
Also, you should be using the builder notation for extraReducers. We will deprecate the object notation you are using soon.
Related
As we all know that we need to make a subscription in useeffect and unsubscribe it when the component will unmount. But this kind of code will be triggered once the component is mounted. I'm now want to trigger the subscription after a specific action.Look at the code below.
const [timing, setTiming] = useState<number>(60)
const interval$ = interval(1000)
useEffect(() => {
})
const sendCodeOnceSubmit = async (phone: number) => {
const res = await sendCode(phone)
if (res.code !== 200) {
message.error(`${res.message}`)
} else {
interval$.pipe(take(60)).subscribe(() => setTiming(timing - 1))
}
}
I have a form in the dom,and once I click submit,the sendCodeOnceSubmit function will be triggered which will then send a request through sendCode function to the server. Once the server return a success code, I want to make a countdown with rxjs, but how can I unsubscribe it cause the normal way to do it is to subscribe a observable in useeffect. Thanks for anyone who can help.
Just wrap interval$ with useState and write a useEffect for it.
// Moved const out of component.
const defaultTiming = 60;
/* ... */
export default function App() {
const [timing, setTiming] = useState<number>(defaultTiming);
const [interval$, setInterval$] = useState<Observable<number> | undefined>();
useEffect(() => {
if (!interval$) return;
const subscription = interval$.pipe(take(defaultTiming)).subscribe(() => {
setTiming((prev) => prev - 1);
});
return () => subscription.unsubscribe();
}, [interval$]);
const sendCodeOnceSubmit = async (phone: number) => {
const res = await sendCode(phone);
if (res.code !== 200) {
// message.error(`${res.message}`);
console.error(res.message);
} else {
setInterval$(interval(1000));
}
};
return (
<div className="App">
<p>{timing}</p>
<button type="button" onClick={() => sendCodeOnceSubmit(123)}>
Click
</button>
</div>
);
}
In have an async request to log a user in:
export const loginUser = createAsyncThunk('users/login', async userInputs => {
try {
const { data } = await axios.post(
'url',
userInputs
);
Cookies.set('user', JSON.stringify(data));
return data;
} catch (error) {
return error.response.data;
}
});
In my store slice I have:
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers(builder) {
// builder.addCase(loginUser.fulfilled, usersAdapter.addOne); // <-- updates state with a delay?
builder.addCase(loginUser.fulfilled, (state, action) => {
state.user = action.payload;
});
},
});
When a user clicks a Log In button the thunk is dispatched. When it returns, the user is redirected to the Home page, which renders conditionally:
useSelector(state => state.user) ? <Home /> : null
With the custom callback this works. However, if I switch to using createEntityAdapter with addOne in the slice:
builder.addCase(loginUser.fulfilled, usersAdapter.addOne);
The useSelector call returns null until I reload the page. Why?
Edit:
The initialState looks like this:
const initialState = {
user: Cookies.get('user') ? JSON.parse(Cookies.get('user')) : null,
};
And the loginHandler:
const loginHandler = async () => {
const data = await dispatch(
loginUser({
email,
password,
})
).unwrap();
navigate('/');
};
I am new to Redux RTK so the problem might not exactly be on calling getSelectors(). However, when I'm using the state that comes from getSelectors() it reloads the entire state.
Problem
The baseline is that I have different Setup objects that I'm calling based on the documentId. These Setup objects are quite large so in the getSetups I am only fetching some basic properties. Then, when the user selects a specific Setup from the dropdown I want to save it in the setupSlice. But when I trigger the dispatch(setSetup(data)) the RTK reloads all the Setups.
I encounter an infinite loop when after fetching all the Setup objects I want to automatically assign the default Setup to the setupSlice.
Extra
Ideally when I assign a Setup to the setupSlice I would like to call the getSetup from RTK to fetch the entire Setup object of that specific Setup and store it in the setupSlice.
I am not sure if this is suppose to be happening but is there anyway to stop it? Otherwise is there any recommendation so I can move forward?
This is the component I'm trying to generate:
const SetupDropdown = () => {
const dispatch = useDispatch()
const { documentId } = useParams()
const { data, isFetching } = useGetSetupsQuery({ documentId })
let setupsMenu;
const { selectAll: selectAllSetups } = getSelectors({documentId})
const allSetups = useSelector(selectAllSetups)
if (!isFetching) {
const defaultSetup = allSetups.find((setup) => setup.default)
setupsMenu = allSetups.map(setup => {
return (<MenuItem value={setup.id}>{setup.name}</MenuItem>)
})
dispatch(setSetup(defaultSetup))
}
const setupId = useSelector(selectSetupId)
const handleChange = async (event) => {
// Here I ideally call the getSetup RTK Query to fetch the entire information of the single setup
const data = {
id: event.target.value,
name: 'Random name'
}
dispatch(setSetup(data))
};
return (
<FormControl sx={{ minWidth: 200 }} size="small">
<InputLabel>Setup</InputLabel>
<Select
value={setupId}
onChange={handleChange}
label="Setup"
>
{setupsMenu}
</Select>
</FormControl>
)
}
export default SetupDropdown;
This is the setupApiSlice:
const setupsAdapter = createEntityAdapter({
sortComparer: (a, b) => b.date.localeCompare(a.date)
})
const initialState = setupsAdapter.getInitialState()
export const setupsApiSlice = apiSlice.injectEndpoints({
tagTypes: ['Setup'],
endpoints: builder => ({
getSetups: builder.query({
query: ({ documentId }) => ({
url: `/documents/${documentId}/setups`,
method: 'GET'
}),
transformResponse: responseData => {
return setupsAdapter.setAll(initialState, responseData)
},
providesTags: (result, error, arg) => [
{ type: 'Setup', id: "LIST" },
...result.ids.map(id => ({ type: 'Setup', id }))
]
}),
getSetup: builder.query({
query: ({ documentId, setupId }) => ({
url: `/documents/${documentId}/setups/${setupId}`,
method: 'GET'
})
})
})
})
export const {
useGetSetupsQuery,
useGetSetupQuery
} = setupsApiSlice
// Define function to get selectors based on arguments (query) of getSetups
export const getSelectors = (
query,
) => {
const selectSetupsResult = setupsApiSlice.endpoints.getSetups.select(query)
const adapterSelectors = createSelector(
selectSetupsResult,
(result) => setupsAdapter.getSelectors(() => result?.data ?? initialState)
)
return {
selectAll: createSelector(adapterSelectors, (s) =>
s.selectAll(undefined)
),
selectEntities: createSelector(adapterSelectors, (s) =>
s.selectEntities(undefined)
),
selectIds: createSelector(adapterSelectors, (s) =>
s.selectIds(undefined)
),
selectTotal: createSelector(adapterSelectors, (s) =>
s.selectTotal(undefined)
),
selectById: (id) => createSelector(adapterSelectors, (s) =>
s.selectById(s, id)
),
}
}
This is the setupSplice:
const initialState = {
name: null,
filters: [],
data: {},
status: 'idle', //'idle' | 'loading' | 'succeeded' | 'failed'
error: null
}
const setupSlice = createSlice({
name: 'setup',
initialState,
reducers: {
setSetup: (state, action) => {
console.log('Dispatch')
const setup = action.payload;
console.log(setup)
state.id = setup.id;
state.name = setup.name;
state.filters = setup.filters;
state.data = setup.state;
state.status = 'succeeded';
}
}
})
export const { setSetup } = setupSlice.actions;
export const selectSetupId = (state) => state.setup.id;
export const selectSetupName = (state) => state.setup.name;
export const selectSetupFilters = (state) => state.setup.filters;
export const selectSetupData = (state) => state.setup.data;
export default setupSlice.reducer;
Tbh., you probably should be using selectFromResult in your useGetSetupsQuery instead of adding another useSelector hook. That would also reduce your code complexity by a lot.
Your problem as hand is that you are creating those selectors within your component on each render - so they don't have a chance to actually memoize and give you a stable result. If you do that in your component, wrap it in a useMemo call to keep your selector instances as stable as possible.
I would like to use useReducer from react-hooks and rxjs together.
For example, I would like to fetch data from an API.
This is the code I wrote in order to do that:
RXJS hook:
function useRx(createSink, data, defaultValue = null) {
const [source, sinkSubscription] = useMemo(() => {
const source = new Subject()
const sink = createSink(source.pipe(distinctUntilChanged()));
const sinkSubscription = sink.subscribe()
return [source, sinkSubscription]
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
source.next(data)
}, [source, data])
useEffect(() => {
return () => {
sinkSubscription.unsubscribe()
};
}, [sinkSubscription])
}
Reducer code:
const dataFetchReducer = (state, action) => {
switch (action.type) {
case 'FETCH_LOADING':
return {
...state,
loading: true
};
case 'FETCH_SUCCESS':
return {
...state,
loading: false,
total: action.payload.total,
data: action.payload.data
};
case 'FETCH_FAILURE':
return {
...state,
error: action.payload
};
case 'PAGE':
return {
...state,
page: action.page,
rowsPerPage: action.rowsPerPage
};
default:
throw new Error();
}
};
How i mix them:
function usePaginationReducerEndpoint(callbackService) {
const defaultPagination = {
statuses: null,
page: 0,
rowsPerPage: 10,
data: [],
total: 0,
error: null,
loading: false
}
const [pagination, dispatch] = useReducer(dataFetchReducer, defaultPagination)
const memoPagination = useMemo(
() => ({
statuses: pagination.statuses,
page: pagination.page,
rowsPerPage: pagination.rowsPerPage
}),
[pagination.statuses, pagination.page, pagination.rowsPerPage]
);
useRx(
memoPagination$ =>
memoPagination$.pipe(
map(memoPagination => {
dispatch({type: "FETCH_LOADING"})
return memoPagination
}),
switchMap(memoPagination => callbackService(memoPagination.statuses, memoPagination.page, memoPagination.rowsPerPage).pipe(
map(dataPagination => {
dispatch({ type: "FETCH_SUCCESS", payload: dataPagination })
return dataPagination
}),
catchError(error => {
dispatch({ type: "FETCH_SUCCESS", payload: "error" })
return of(error)
})
))
),
memoPagination,
defaultPagination,
2000
);
function handleRowsPerPageChange(event) {
const newTotalPages = Math.trunc(pagination.total / event.target.value)
const newPage = Math.min(pagination.page, newTotalPages)
dispatch({
type: "PAGE",
page: newPage,
rowsPerPage: event.target.value
});
}
function handlePageChange(event, page) {
dispatch({
type: "PAGE",
page: page,
rowsPerPage: pagination.rowsPerPage
});
}
return [pagination, handlePageChange, handleRowsPerPageChange]
}
The code works but I'm wondering if this is luck or not...
Is it ok if I dispatch inside RXJS pipe ? What is the risk ?
If no, how can I mix both useReducer and RXJS ?
If it's not the good approach, what is the good one ?
I know this ressource: https://www.robinwieruch.de/react-hooks-fetch-data/. But I would like to mix the power of hooks and RXJS in order to use, for example, the debounce function with rxjs in an async request...
Thanks for your help,
All you need is a middleware to connect useReducer and rxjs rather than create one yourself.
Using useReducer will create a lot of potential hard to debug code and also need an independent container component to put useReducer to prevent accident global rerendering.
So I suggest that using redux places useReducer to create global state from a component and use redux-observable (RxJS 6-based middleware for Redux) as middleware to connect rxjs and redux.
if you know rxjs well, it will be very easy to use, as official web show, fetch data from api will be:
https://redux-observable.js.org/docs/basics/Epics.html
// epic
const fetchUserEpic = action$ => action$.pipe(
ofType(FETCH_USER),
mergeMap(action =>
ajax.getJSON(`https://api.github.com/users/${action.payload}`).pipe(
map(response => fetchUserFulfilled(response))
)
)
);
I have problem with my async action. I would like to set 'loading' state to true when action fetchPosts() is called and 'loading' state to false when action fetchPostsSuccess() or fetchPostsFailiure().
With my current code it works almost fine except 'loading' state change when fetchPosts() receive response from server and I would like to change this state at the beginning of request.
Here is simple code which shows my steps.
I'm using axios and redux-promise (https://github.com/acdlite/redux-promise).
// actions
export function fetchPosts() {
const request = axios.get(`${API_URL}/posts/`);
return {
type: 'FETCH_POSTS',
payload: request,
};
}
export function fetchPostsSuccess(posts) {
return {
type: 'FETCH_POSTS_SUCCESS',
payload: posts,
};
}
export function fetchPostsFailure(error) {
return {
type: 'FETCH_POSTS_FAILURE',
payload: error,
};
}
// reducer
const INITIAL_STATE = {
posts: [],
loading: false,
error: null,
}
const postsReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case 'FETCH_POSTS':
return { ...state, loading: true, error: null };
case 'FETCH_POSTS_SUCCESS':
return { ...state, posts: action.payload, loading: false };
case 'FETCH_POSTS_FAILURE':
return { ...state, posts: [], loading: false, error: action.payload };
default:
return state;
}
}
const rootReducer = combineReducers({
postsList: postsReducer,
});
// store
function configureStore(initialState) {
return createStore(
rootReducer,
applyMiddleware(
promise,
),
);
}
const store = configureStore();
// simple Posts app
class Posts extends Component {
componentWillMount() {
this.props.fetchPosts();
}
render() {
const { posts, loading } = this.props.postsList;
return (
<div>
{loading && <p>Loading...</p>}
<ul>
{posts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
</div>
);
}
}
const mapStateToProps = state => ({
postsList: state.postsList,
});
const mapDispatchToProps = dispatch => ({
fetchPosts: (params = {}) => {
dispatch(fetchPosts())
.then((response) => {
if (!response.error) {
dispatch(fetchPostsSuccess(response.payload.data));
} else {
dispatch(fetchPostsFailure(response.payload.data));
}
});
},
});
const PostsContainer = connect(mapStateToProps, mapDispatchToProps)(Posts);
// main
ReactDOM.render((
<Provider store={store}>
<Router history={browserHistory}>
<Route path="posts" component={PostsContainer} />
</Router>
</Provider>
), document.getElementById('appRoot'));
Can someone guide me what I'm doing wrong ?
It's turned out the problem is with 'redux-promise' package. This async middleware has no such thing like 'pending' state of promise (called 'optimistic update') .
It changes the state only when promise has been resolved or rejected.
I should use different middleware which allow for 'optimistic updates'
Your problem ís with redux-promise. You should use redux-thunk instead that allows you to return a function and dispatch multiple times. Have a look at it ;)!