how can i fetchMagazineListDetail function arugment memoizing when i call request second time if already
this argument is exist i dont want sent the request.
export const fetchMagazineListDetail = (id, paginate) => {
return dispatch => {
dispatch(fetchMagazineListDetailBegin());
fetch(`http://localhost:3003/magazine/${paginate}`)
.then(res => res.json())
.then(res => {
if(res.error) {
throw(res.error);
}
dispatch(fetchMagazineListDetailSuccess(res));
return res;
})
.catch(error => {
dispatch(fetchMagazineListDetailError(error));
})
}
}
Thanks in advance
You can group the action and not dispatch it when it was already dispatched. Depending on the cache you can keep result during render, during app active or in local storage:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;
const { useEffect, useMemo, useState } = React;
const { produce } = immer;
//group promise returning function
const createGroup = (cache) => (
fn,
getKey = (...x) => JSON.stringify(x)
) => (...args) => {
const key = getKey(args);
let result = cache.get(key);
if (result) {
return result;
}
//no cache
result = Promise.resolve(fn.apply(null, args)).then(
(r) => {
cache.resolved(key); //tell cache promise is done
return r;
},
(e) => {
cache.resolve(key); //tell cache promise is done
return Promise.reject(e);
}
);
cache.set(key, result);
return result;
};
//thunk action creators are not (...args)=>result but
// (...args)=>(dispatch,getState)=>result
// so here is how we group thunk actions
const createGroupedThunkAction = (thunkAction, cache) => {
const group = createGroup(
cache
)((args, dispatch, getState) =>
thunkAction.apply(null, args)(dispatch, getState)
);
return (...args) => (dispatch, getState) => {
return group(args, dispatch, getState);
};
};
//permanent memory cache store creator
const createPermanentMemoryCache = (cache = new Map()) => {
return {
get: (key) => cache.get(key),
set: (key, value) => cache.set(key, value),
resolved: (x) => x,
};
};
const NOT_REQUESTED = { requested: false };
const initialState = { data: {} };
//action types
const BEGIN = 'BEGIN';
const SUCCESS = 'SUCCESS';
//action creators
const fetchMagazineListDetail = createGroupedThunkAction(
(id) => {
return (dispatch) => {
//return a promise
return Promise.resolve().then(() => {
dispatch({ type: BEGIN, payload: { id } });
setTimeout(() =>
dispatch({ type: SUCCESS, payload: { id } })
);
});
};
},
createPermanentMemoryCache()
);
const reducer = (state, { type, payload }) => {
if (type === BEGIN) {
return produce(state, (draft) => {
const { id } = payload;
draft.data[id] = { ...state.data[id] };
draft.data[id].loading = true;
draft.data[id].requested = true;
});
}
if (type === SUCCESS) {
const { id } = payload;
return produce(state, (draft) => {
draft.data[id].loading = false;
draft.data[id].requested = true;
draft.data[id].result = payload;
});
}
return state;
};
//selectors
const selectData = (state) => state.data;
const crateSelectDataById = (id) =>
createSelector([selectData], (data) =>
data[id] ? data[id] : NOT_REQUESTED
);
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
//thunk
({ dispatch, getState }) => (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
}
)
)
);
const Data = React.memo(function Data({ id }) {
const dispatch = useDispatch();
const selectDataById = useMemo(
() => crateSelectDataById(id),
[id]
);
const result = useSelector(selectDataById);
useEffect(() => {
if (!result.requested) {
dispatch(fetchMagazineListDetail(id));
}
}, [dispatch, id, result.requested]);
return <pre>{JSON.stringify(result, undefined, 2)}</pre>;
});
const App = () => {
const [id, setId] = useState(1);
return (
<div>
<select
value={id}
onChange={(e) => setId(e.target.value)}
>
<option value={1}>1</option>
<option value={2}>2</option>
</select>
<Data id={id} />
<Data id={id} />
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<script src="https://unpkg.com/immer#7.0.5/dist/immer.umd.production.min.js"></script>
<div id="root"></div>
During one render the fetchMagazineListDetail is dispatched twice but if you check the redux dev tools you can see it is actually only dispatched once. When changing the id to an id that was used earlier you see nothing will be dispatched.
If you don't care about dispatching the actions multiple times and just want to cache responses then you can do the following:
const apiCall = (x) => x;
const groupedApiCall = createGroup(
createPermanentMemoryCache()
)(apiCall);
You could also solve the problem in the service worker as that's what was created to cache responses.
Related
I'm trying to display the response from the API into my react component but it's not working. If I try to use it in the console, I can see the data and its value but not in the react component, it's empty when I try to show the value in a div.
Here is the code where I'm trying to display it in my react component:
const CharacterListing = () => {
const characters = useSelector(getAllCharacters);
console.log("Hello", characters);
const renderCharacters = Object.entries(characters).map(([key, value]) => {
console.log(value.name);
<div>{value.name}</div>
})
return (
<div>
{renderCharacters}
</div>
);
};
export default CharacterListing;
This is the code for my Character Slice Component
const initialState = {
characters: {},
};
const characterSlice = createSlice({
name: 'characters',
initialState,
reducers: {
addCharacters: (state, { payload }) => {
state.characters = payload;
},
},
});
export const { addCharacters } = characterSlice.actions;
export const getAllCharacters = (state) => state.characters.characters;
export default characterSlice.reducer;
This is the code for my Home Component:
const Home = () => {
const dispatch = useDispatch();
useEffect(() => {
const fetchCharacters = async () => {
const response = await baseURL.get(`/characters`)
.catch(error => {
console.log("Error", error);
});
dispatch(addCharacters(response.data));
console.log("Success", response);
};
fetchCharacters();
}, [])
return (
<div>
Home
<CharacterListing />
</div>
);
};
export default Home;
Thank you
You forgot to return item into your map func
Try this :
const renderCharacters = Object.entries(characters).map(([key, value]) => {
console.log(value.name);
return <div key={key}>{value.name}</div>
})
I am trying to return the value from function that has the onSnapshot() event but keep getting this weird error. Basically, I call this action and return the data from it like I would in any other function. But I keep getting this error and I do not know how to fix it.
This is the error
Uncaught TypeError: Cannot add property 0, object is not extensible
at Array.push (<anonymous>)
This the function
export const getQuestions = () => {
var questions = [];
onSnapshot(collection(firebaseDatabase, "questions"), (querySnapshot) => {
querySnapshot.docs.forEach((doc) => {
if (doc.data() !== null) {
questions.push(doc.data());
}
});
});
return questions;
};
Also this function is used with Redux Thunk and Redux Toolkit.
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import { getQuestions } from "../../utils/firebase-functions/firebase-functions";
export const getAllQuestions = createAsyncThunk(
"allQuestions/getAllQuestions",
async () => {
const response = getQuestions();
return response;
}
);
export const allQuestionsSlice = createSlice({
name: "allQuestions",
initialState: {
allQuestions: [],
loading: false,
error: null,
},
extraReducers: {
[getAllQuestions.pending]: (state) => {
state.loading = true;
state.error = null;
},
[getAllQuestions.fulfilled]: (state, action) => {
state.allQuestions = action.payload;
state.loading = false;
state.error = null;
},
[getAllQuestions.rejected]: (state, action) => {
state.loading = false;
state.error = action.payload;
},
},
});
export default allQuestionsSlice.reducer;
Where it is dispatched
const dispatch = useDispatch();
const tabContentData = useSelector(
(state) => state.allQuestions.allQuestions
);
useEffect(() => {
dispatch(getAllQuestions());
}, [dispatch]);
console.log(tabContentData);
You can try returning a promise when the data is being fetch for first time as shown below:
let dataFetched = false;
export const getQuestions = () => {
return new Promise((resolve, reject) => {
onSnapshot(collection(firebaseDatabase, "questions"), (querySnapshot) => {
querySnapshot.docs.forEach((doc) => {
if (doc.data() !== null) {
questions.push(doc.data());
}
});
if (!dataFetched) {
// data was fetched first time, return all questions
const questions = querySnapshot.docs.map(q => ({ id: q.id, ...q.data()}))
resolve(questions)
dataFetched = true;
} else {
// Questions already fetched,
// TODO: Update state with updates received
}
});
})
};
getQuestions() now returns a Promise so add an await here:
const response = await getQuestions();
For updates received later, you'll have to update them directly in your state.
I created a class object that stores the postlist.
When the post was updated, the Pub-Sub pattern subscription function was registered.
class PostLoad extends PubSub {
store = [];
constructor (){
super();
this.connectSocket = connectSocket(this.projectUid, async (type, post) => {
console.log('Socket HOOK', type, post);
if (type === 'insert') await this.newPostPush(post);
if (type === 'update') {
const {deleted} = post;
if (deleted) await this.deletePostInStore(post.id);
}
});
}
async postLoad () {
// ...API LOAD
const value = axios.get('');
this.store = value;
}
private async newPostPush(post: HashsnapPostItem) {
if (post.type === 'video') return;
const storeItem = {...post, img: await loadImage(post.url)} as HashsnapPost;
this.store.unshift(storeItem);
if (this.store.length > this.limit) this.store.splice(this.limit);
this.emit('insert', {post: storeItem, store: this.store});
}
private deletePostInStore(targetId: number) {
this.store.push(...this.store.splice(0).filter(({id}) => id !== targetId));
this.emit('delete', {postId: targetId, store: this.store});
}
}
React component executes events registered in Pub-Sub,
When the post was updated, the status value was changed, but there was no change.
const PostList = () => {
const [postList, setPostList] = useState([]);
const postLoad = store.get('postLoad'); // PostLoad Class Object
useEffect(() => {
setPostList(postLoad.store);
}, []);
postLoad.on('delete', (payload) => {
setPostList(postLoad.store);
})
postLoad.on('insert', (payload) => {
setPostList(postLoad.store);
})
return <div>
{postList.map((post, i) => {
return <div key={`post-${i}`}></div>
})}
</div>
}
What I want is that when the Pus-Sub event is executed, the status value changes and re-rends.
async postLoad () {
// ...API LOAD- axios is a async func, need await this
const value = await axios.get('');
this.store = value;
}
postLoad.on('delete', (payload) => {
setPostList(postLoad.store);
})
postLoad.on('insert', (payload) => {
setPostList(postLoad.store);
})
// thest two register on postLoad will repeat many times, just use hook useEffect, if postLoad always change, useRef, so the right code is below
const PostList = () => {
const [postList, setPostList] = useState([]);
const {current: postLoad} = useRef(store.get('postLoad'));
useEffect(() => {
setPostList(postLoad.store);
postLoad.on('delete', (payload) => {
setPostList(postLoad.store);
})
postLoad.on('insert', (payload) => {
setPostList(postLoad.store);
})
return () => {
// please unregister delete and insert here
}
}, [postLoad]);
return <div>
{postList.map((post, i) => {
return <div key={`post-${i}`}></div>
})}
</div>
}
This is the component
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { deleteSong, getSongs } from '../../store/song';
import ReactAudioPlayer from 'react-audio-player';
const SongList = () => {
const dispatch = useDispatch();
const songsObj = useSelector((state) => state.songState.entries);
const songs = Object.values(songsObj)
const user = useSelector((state) => state.session.user);
const CurrentUserId = user?.id
const userSongs = songs.filter(song => song.userId === CurrentUserId)
const remove = async () => {
const songId = 10
dispatch(deleteSong(songId));
}
useEffect(() => {
dispatch(getSongs());
}, [dispatch]);
return (
<div>
<h1>Song List</h1>
<ol>
{songs.map(({ id, songName, songLink, userId }) => (
<div className='songdetails' key={id}>
<p key={id}>songName={songName}</p>
{userId === CurrentUserId ?
<div>
<button id={id} onClick={remove}>remove</button>
</div>
: null}
</div>
))}
</ol>
</div>
);
};
export default SongList;
These are the routes
router.get('/', asyncHandler(async (req, res) => {
const songs = await Song.findAll();
res.json(songs);
}));
router.post('/:id', requireAuth, songValidations.validateCreate, asyncHandler(async (req, res) => {
const song = await Song.create(req.body);
return res.json(song);
}));
router.delete('/:id', requireAuth, asyncHandler(async function (req, res) {
const song = await Song.findByPk(req.params.id)
if (!song) throw new Error('Cannot find song');
await Song.destroy({ where: { id: song.id } });
return song.id
}));
And the store
import { csrfFetch } from "./csrf";
const LOAD_SONGS = 'song/loadSongs';
const ADD_SONG = 'song/addSongs';
const REMOVE_SONG = 'song/removeSongs'
export const loadSongs = (songs) => ({
type: LOAD_SONGS,
songs
});
export const getSongs = () => async (dispatch) => {
const response = await fetch('/api/songs')
const songs = await response.json()
dispatch(loadSongs(songs))
return songs
}
export const addSong = (newSong) => ({
type: ADD_SONG,
newSong
});
export const postSong = (newSong) => async (dispatch) => {
const response = await csrfFetch(`/api/songs/${newSong.id}`, {
method: 'post',
body: JSON.stringify(newSong)
});
const song = await response.json();
if (response.ok) {
dispatch(addSong(song));
return song;
}
};
export const removeSong = (songId) => ({
type: REMOVE_SONG,
songId,
});
export const deleteSong = (songId) => async (dispatch) => {
console.log(songId, 'deleteSong song')
const response = await csrfFetch(`/api/songs/${songId}`, {
method: 'delete'
});
if (response.ok) {
const song = await response.json();
dispatch(removeSong(song.id));
}
};
const initialState = { entries: {} };
const songReducer = (state = initialState, action) => {
switch (action.type) {
case LOAD_SONGS: {
const newState = { ...state, entries: {} }
action.songs.forEach(song => {
newState.entries[song.id] = song
})
return newState
}
case ADD_SONG: {
return {
...state,
entries: {
...state.entries,
[action.newSong.id]: action.newSong
}
}
}
case REMOVE_SONG: {
const newState = { ...state, entries: { ...state.entries } };
delete newState.entries[action.songId];
return newState;
}
default:
return state;
}
};
export default songReducer;
Specifically I'm having trouble getting the specific song I want to delete. I can render the user's songs with the option to delete just fine but how should I tell it what song I'm specifically clicking on so it knows to delete. Also the state is not updating as well but I think that is related to my current issue. Any refactoring tips as well would be appreciated, this is my first react project. Thanks!
...
const remove = (e) => {
dispatch(deleteSong(e.target.id))
}
...
return (
...
<button id={id} onClick={remove}>remove</button>
I found myself continuously writing the same shape of code for asynchronous calls so I tried to wrap it up in something that would abstract some of the details. What I was hoping was that in my onError callback I could pass a reference of the async function being executed so that some middleware could implement retry logic if it was necessary. Maybe this is a code smell that I'm tackling this the wrong way but I'm curious if it's possible or if there are other suggestions for handling this.
const runAsync = (asyncFunc) => {
let _onBegin = null;
let _onCompleted = null;
let _onError = null;
let self = this;
return {
onBegin(f) {
_onBegin = f;
return this;
},
onCompleted(f) {
_onCompleted = f;
return this;
},
onError(f) {
_onError = f;
return this;
},
async execute() {
if (_onBegin) {
_onBegin();
}
try {
let data = await asyncFunc();
if (_onCompleted) {
_onCompleted(data);
}
} catch (e) {
if (_onError) {
_onError(e ** /*i'd like to pass a function reference here as well*/ ** );
}
return Promise.resolve();
}
},
};
};
await runAsync(someAsyncCall())
.onBegin((d) => dispatch(something(d)))
.onCompleted((d) => dispatch(something(d)))
.onError((d, func) => dispatch(something(d, func)))
.execute()
I'm thinking you could use a custom hook. Something like -
import { useState, useEffect } from 'react'
const useAsync = (f) => {
const [state, setState] =
useState({ loading: true, result: null, error: null })
const runAsync = async () => {
try {
setState({ ...state, loading: false, result: await f })
}
catch (err) {
setState({ ...state, loading: false, error: err })
}
}
useEffect(_ => { runAsync() }, [])
return state
}
Now we can use it in a component -
const FriendList = ({ userId }) => {
const response =
useAsync(UserApi.fetchFriends(userId)) // <-- some promise-returning call
if (response.loading)
return <Loading />
else if (response.error)
return <Error ... />
else
return <ul>{response.result.map(Friend)}</ul>
}
The custom hook api is quite flexible. The above approach is naive, but we can think it through a bit more and make it more usable -
import { useState, useEffect } from 'react'
const identity = x => x
const useAsync = (runAsync = identity, deps = []) => {
const [loading, setLoading] = useState(true)
const [result, setResult] = useState(null)
const [error, setError] = useState(null)
useEffect(_ => {
Promise.resolve(runAsync(...deps))
.then(setResult, setError)
.finally(_ => setLoading(false))
}, deps)
return { loading, error, result }
}
Custom hooks are dope. We can make custom hooks using other custom hooks -
const fetchJson = (url = "") =>
fetch(url).then(r => r.json()) // <-- stop repeating yourself
const useJson = (url = "") => // <-- another hook
useAsync(fetchJson, [url]) // <-- useAsync
const FriendList = ({ userId }) => {
const { loading, error, result } =
useJson("some.server/friends.json") // <-- dead simple
if (loading)
return <Loading .../>
if (error)
return <Error .../>
return <ul>{result.map(Friend)}</ul>
}