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>
}
Related
I have the following React component:
const AuthInit: FC<WithChildren> = ({children}) => {
const {auth, logout, setCurrentUser} = useAuth()
const didRequest = useRef(false)
const [showSplashScreen, setShowSplashScreen] = useState(true)
// We should request user by authToken (IN OUR EXAMPLE IT'S API_TOKEN) before rendering the application
useEffect(() => {
const requestUser = async (apiToken: string) => {
try {
if (!didRequest.current) {
const {data} = await getUserByToken(apiToken)
if (data) {
setCurrentUser(data)
}
}
} catch (error) {
console.error(error)
if (!didRequest.current) {
logout()
}
} finally {
setShowSplashScreen(false)
}
return () => (didRequest.current = true)
}
if (auth && auth.api_token) {
requestUser(auth.api_token)
} else {
logout()
setShowSplashScreen(false)
}
// eslint-disable-next-line
}, [])
return showSplashScreen ? <LayoutSplashScreen /> : <>{children}</>
}
The component contains this line of code:
return () => (didRequest.current = true)
How can I deconstruct this line into multiple lines so that I can put a breakpoint on didRequest.current = true?
function updateDidRequest = () => {
didRequest.current = true;
}
return updateDidRequest;
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.
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 have a react component that has a html button that when clicked calls a function that adds an element to a redux reducer and then redirects to another component. The component that is redirected to needs to set state from the reducer but it won't. I know that it is being added to the array in the reducer because I wrote it as an async await and it redirects after it gets added.
This is the original component
const Posts = () => {
const dispatch = useDispatch();
const getProfile = async (member) => {
await dispatch({ type: 'ADD_MEMBER', response: member })
console.log(member)
window.location.href='/member'
console.log('----------- member------------')
console.log(post)
}
const socialNetworkContract = useSelector((state) => state.socialNetworkContract)
return (
<div>
{socialNetworkContract.posts.map((p, index) => {
return <tr key={index}>
<button onClick={() => getProfile(p.publisher)}>Profile</button>
</tr>})}
</div>
)
}
export default Posts;
This is the 'socialNetworkContract' reducer
import { connect, useDispatch, useSelector } from "react-redux";
let init = {
posts:[],
post:{},
profiles:[],
profile:{},
members:[],
member:{}
}
export const socialNetworkContract = (state = init, action) => {
const { type, response } = action;
switch (type) {
case 'ADD_POST':
return {
...state,
posts: [...state.posts, response]
}
case 'SET_POST':
return {
...state,
post: response
}
case 'ADD_PROFILE':
return {
...state,
profiles: [...state.profiles, response]
}
case 'SET_PROFILE':
return {
...state,
profile: response
}
case 'ADD_MEMBER':
return {
...state,
members: [...state.members, response]
}
case 'SET_MEMBER':
return {
...state,
member: response
}
default: return state
}
};
and this is the component that the html button redirects to
const Member = () => {
const [user, setUser] = useState({})
const [profile, setProfile] = useState({});
const dispatch = useDispatch();
useEffect(async()=>{
try {
const pro = socialNetworkContract.members[0];
setUser(pro)
const p = await incidentsInstance.usersProfile(user, { from: accounts[0] });
const a = await snInstance.getUsersPosts(user, { from: accounts[0] });
console.log(a)
setProfile(p)
} catch (e) {
console.error(e)
}
}, [])
const socialNetworkContract = useSelector((state) => state.socialNetworkContract)
return (
<div class="container">
{socialNetworkContract.posts.map((p, index) => {
return <tr key={index}>
{p.message}
{p.replies}
</tr>})}
</div>
</div>
</div>
</div>
)
}
export default Member;
This is the error I get in the console
Error: invalid address (arg="user", coderType="address", value={})
The functions I'm calling are solidity smart contracts and the have been tested and are working and the element I'm trying to retrieve out of the array is an ethereum address.
incidentsInstance and snInstance are declared in the try statement but I took a lot of the code out to make it easier to understand.
given setUser is async, your user is still an empty object when you make your request.
you could pass pro value instead:
useEffect(async () => {
try {
const pro = socialNetworkContract.members[0];
setUser(pro)
const p = await incidentsInstance.usersProfile(pro, { from: accounts[0] });
const a = await snInstance.getUsersPosts(pro, { from: accounts[0] });
setProfile(p)
} catch (e) {
console.error(e)
}
}, [])
or break your useEffect in two pieces:
useEffect(() => {
setUser(socialNetworkContract.members[0]);
}, [])
useEffect(async () => {
if (!Object.keys(user).length) return;
try {
const p = await incidentsInstance.usersProfile(user, { from: accounts[0] });
const a = await snInstance.getUsersPosts(user, { from: accounts[0] });
console.log(a)
setProfile(p)
} catch (e) {
console.error(e)
}
}, [user])
note: fwiw, at first sight your user state looks redundant since it's derived from a calculated value.
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.