Redux toolkit: updating state with entityAdapter vs custom callback - react-redux

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('/');
};

Related

How make make await as useDispatch

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.

Redux connected React component not updating until a GET api request is recalled

My react app uses a redux connected component to render data from backend for a project page, so I called a GET dispatch inside a React Hook useEffect to make sure data is always rendered when the project page first open, and whenever there is a change in state project, the component will be updated accordingly using connect redux function. However, the component doesn't update after I reduce the new state using a DELETE API request, only if I dispatch another GET request then the state will be updated. So I have to call 2 dispatches, one for DELETE and one for GET to get the page updated synchronously (as you can see in handleDeleteUpdate function), and the same thing happened when I dispatch a POST request to add an update (in handleProjectUpdate). Only when I reload the page, the newly changed data will show up otherwise it doesn't happen synchronously, anyone knows what's wrong with the state update in my code? and how can I fix this so the page can be loaded faster with only one request?
I've changed the reducer to make sure the state is not mutated and is updated correctly.
I have also tried using async function in handleDeleteUpdate to make sure the action dispatch is finished
I have tried
console.log(props.project.data.updates)
to print out the updates list after calling props.deleteUpdate but it seems the updates list in the state have never been changed, but when I reload the page, the new updates list is shown up
Here is the code I have for the main connected redux component, actions, and reducers file for the component
function Project(props) {
let options = {year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit'}
const {projectID} = useParams();
const history = useHistory();
console.log(props.project.data? props.project.data.updates : null);
console.log(props.project.data);
// const [updates, setUpdates] = useState(props.project.data? props.project.data.updates : null)
useEffect(() => {
props.getProject(projectID);
}, []);
// Add an update to project is handled here
const handleProjectUpdate = async (updateInfo) => {
await props.postProjectUpdate(projectID, updateInfo)
await props.getProject(projectID);
}
const handleDeleteUpdate = async (updateID) => {
await props.deleteUpdate(projectID, updateID);
await props.getProject(projectID);
console.log(props.project.data.updates);
};
return (
<div>
<Navbar selected='projects'/>
<div className = "project-info-layout">
<UpdateCard
updates = {props.project.data.updates}
handleProjectUpdate = {handleProjectUpdate}
handleDeleteUpdate = {handleDeleteUpdate}
options = {options}
/>
</div>
</div>
)
}
const mapStateToProps = state => ({
project: state.project.project,
});
export default connect(
mapStateToProps,
{getProject, postProjectUpdate, deleteUpdate}
)(Project);
ACTION
import axios from 'axios';
import { GET_PROJECT_SUCCESS,ADD_PROJECT_UPDATE_SUCCESS, DELETE_PROJECT_UPDATE_SUCCESS} from './types';
let token = localStorage.getItem("token");
const config = {
headers: {
Authorization: `Token ${token}`,
}
};
export const getProject = (slug) => dispatch => {
axios.get(`${backend}/api/projects/` + slug, config)
.then(
res => {
dispatch({
type: GET_PROJECT_SUCCESS,
payload: res.data,
});
},
).catch(err => console.log(err));
}
export const postProjectUpdate = (slug, updateData) => dispatch => {
axios.post(`${backend}/api/projects/`+slug+ `/updates`,updateData, config)
.then(
res => {
dispatch({
type: ADD_PROJECT_UPDATE_SUCCESS,
payload: res.data,
});
},
).catch(err => console.log(err));
}
export const deleteUpdate = (slug, updateID) => dispatch => {
axios.delete(`${backend}/api/projects/`+ slug + `/updates/`+ updateID, config)
.then(
res => {
dispatch({
type: DELETE_PROJECT_UPDATE_SUCCESS,
payload: updateID,
});
},
).catch(err => console.log(err));
}
Reducer
import { GET_PROJECT_SUCCESS,ADD_PROJECT_UPDATE_SUCCESS, DELETE_PROJECT_UPDATE_SUCCESS} from "../actions/types";
const initialState = {
project: {},
};
export default function ProjectReducer(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case GET_PROJECT_SUCCESS:
return {
...state, // return all initial state
project: payload
};
case ADD_PROJECT_UPDATE_SUCCESS:
return {
...state,
project: {
...state.project,
updates: [...state.project.data.updates, payload.data]
}
};
case DELETE_PROJECT_UPDATE_SUCCESS:
let newUpdatesArray = [...state.project.updates]
newUpdatesArray.filter(update => update.uuid !== payload)
return {
...state,
project: {
...state.project,
members: newUpdatesArray
}
};
default:
return state;
}
}
updateCard in the Project component is showing a list of all updates

How to listen emit event in parent component in vue3

I want to pass event from child comment to parent.
I did same thing in vue2 but i don't know how to that in vue3.
This one is child component setup method.
setup(props, { emit }) {
const router = useRouter();
const form = ref(
{
email: "ajay#gmail.com",
password: "123456789",
isLoading: false,
},
);
const user = ref("");
const error = ref("");
function login() {
User.login(this.form).then(() => {
emit('login', true);
// this.$root.$emit("login", true); -- vue2
localStorage.setItem("auth", "true");
router.push('/dashboard');
})
.catch(error => {});
}
return { form, login, user, error};
}
from here emit login method and i want to listen in parent comment.
this is parent component, emit.on method not working here
setup(props, { emit }) {
const router = useRouter();
const state = reactive({
isLoggedIn: false,
});
onMounted(async () => {
emit.on("login", () => { // `vue2` this.$root.$on("login"`
this.isLoggedIn = true;
});
});
In parent component you should add a handler for that emitted event :
<child #login="onLogin"></child>
setup(props, { emit }) {
const router = useRouter();
const state = reactive({
isLoggedIn: false,
});
function onLogin(){
state.isLoggedIn=true,
}
return{state,onLogin}
}
Or make a composable function named useAuth in separate file :
import {reactive} from 'vue'
const state = reactive({
isLoggedIn: false,
});
const useAuth=()=>{
function onLogin(){
state.isLogged=true;
}
return {state,onLogin}
}
export default useAuth();
then import the function inside the two components :
child :
import useAuth from './useAuth'
....
setup(props, { emit }) {
const router = useRouter();
const {useAuth} =useAuth();
....
function login() {
User.login(this.form).then(() => {
onLogin() //will call the nested function that set loggedIn to true
localStorage.setItem("auth", "true");
router.push('/dashboard');
})
.catch(error => {});
}
in parent :
import useAuth from './useAuth'
....
setup(props, { emit }) {
const router = useRouter();
const {state} =useAuth();
//it replaces your local state

Redux async action triggered after request finished. Why?

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 ;)!

Redux action ajax result not dispatched to reducer

I just get to experiment with Redux and I know that middleware is essential to make ajax calls. I've installed redux-thunk and axios package separately and tried to hook my result as a state and render the ajax result to my component. However my browser console displays an error and my reducer couldn't grab the payload.
The error:
Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.
This is part of my code and how the middleware is hooked up:
//after imports
const logger = createLogger({
level: 'info',
collapsed: true,
});
const router = routerMiddleware(hashHistory);
const enhancer = compose(
applyMiddleware(thunk, router, logger),
DevTools.instrument(),
persistState(
window.location.href.match(
/[?&]debug_session=([^&]+)\b/
)
)
// store config here...
my action:
import axios from 'axios';
export const SAVE_SETTINGS = 'SAVE_SETTINGS';
const url = 'https://hidden.map.geturl/?with=params';
const request = axios.get(url);
export function saveSettings(form = {inputFrom: null, inputTo: null}) {
return (dispatch) => {
dispatch(request
.then((response) => {
const alternatives = response.data.alternatives;
var routes = [];
for (const alt of alternatives) {
const routeName = alt.response.routeName;
const r = alt.response.results;
var totalTime = 0;
var totalDistance = 0;
var hasToll = false;
// I have some logic to loop through r and reduce to 3 variables
routes.push({
totalTime: totalTime / 60,
totalDistance: totalDistance / 1000,
hasToll: hasToll
});
}
dispatch({
type: SAVE_SETTINGS,
payload: { form: form, routes: routes }
});
})
);
}
}
reducer:
import { SAVE_SETTINGS } from '../actions/configure';
const initialState = { form: {configured: false, inputFrom: null, inputTo: null}, routes: [] };
export default function configure(state = initialState, action) {
switch (action.type) {
case SAVE_SETTINGS:
return state;
default:
return state;
}
}
you can see the state routes has size of 0 but the action payload has array of 3.
Really appreciate any help, thanks.
It looks like you have an unnecessary dispatch in your action, and your request doesn't look to be instantiated in the correct place. I believe your action should be:
export function saveSettings(form = { inputFrom: null, inputTo: null }) {
return (dispatch) => {
axios.get(url).then((response) => {
...
dispatch({
type: SAVE_SETTINGS,
payload: { form: form, routes: routes }
});
});
};
}

Resources