rewrite componentDidUpdate(prevProps) into a hook with redux - javascript

I would like to rewrite this life cycle method into a hook but it does'nt work as expected.
when the componentdidmounted, if the user id exists in the local storage,the user is connected and his name is displayed in the navbar. And when he disconnects and reconnects his name is displayed in the navbar.
So i am trying to convert this class Component with hooks, when the username changes nothing is displayed in the navbar so i have to refresh the page and that way his name is displayed
The real problem is the componentDidUpdate
how can i get and compare the prevProps with hooks
The class Component
const mapStateToProps = state => ({
...state.authReducer
}
);
const mapDispatchToProps = {
userSetId,
userProfilFetch,
userLogout
};
class App extends React.Component {
componentDidMount() {
const userId = window.localStorage.getItem("userId");
const {userSetId} = this.props;
if (userId) {
userSetId(userId)
}
}
componentDidUpdate(prevProps, prevState, snapshot) {
const {userId, userProfilFetch, userData} = this.props; //from redux store
if(prevProps.userId !== userId && userId !== null && userData === null){
userProfilFetch(userId);
}
}
render() {
return (
<div>
<Router>
<Routes/>
</Router>
</div>
);
}
}
export default connect(mapStateToProps,mapDispatchToProps)(App);
With hooks
const App = (props) => {
const dispatch = useDispatch();
const userData = useSelector(state => state.authReducer[props.userData]);
const userId = window.localStorage.getItem("userId");
useEffect(()=> {
if(!userId){
dispatch(userSetId(userId))
dispatch(userProfilFetch(userId))
}
}, [userData, userId, dispatch])
return(
<Router>
<Routes/>
</Router>
)
};
export default App;

How to get the previous props or state?
Basically create a custom hook to cache a value:
const usePrevious = value => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
Usage:
const App = (props) => {
const dispatch = useDispatch();
const userData = useSelector(state => state.authReducer[props.userData]);
const userId = window.localStorage.getItem("userId");
// get previous id and cache current id
const prevUserId = usePrevious(userId);
useEffect(()=> {
if(!userId){
dispatch(userSetId(userId))
dispatch(userProfileFetch(userId))
}
// do comparison with previous and current id value
if (prevUserId !== userId) {
dispatch(userProfileFetch(userId));
}
}, [userData, userId, prevUserId, dispatch])
return(
<Router>
<Routes/>
</Router>
)
};
FYI: You may want to refactor the code a bit to do the fetch from local storage in an effect hook that runs only on mount. If I understand your app flow correctly it would look something like this:
const App = (props) => {
const dispatch = useDispatch();
const { userId } = useSelector(state => state.authReducer[props.userData]);
useEffect(() => {
const userId = window.localStorage.getItem("userId");
userId && dispatch(userSetId(userId));
}, []);
// get previous id and cache current id
const prevUserId = usePrevious(userId);
useEffect(()=> {
if(!userId){
dispatch(userSetId(userId))
dispatch(userProfileFetch(userId))
}
// do comparison with previous and current id value
if (prevUserId !== userId) {
dispatch(userProfileFetch(userId));
}
}, [userId, prevUserId, dispatch])
return(
<Router>
<Routes/>
</Router>
)
};

now i resolve it, i made this
const App = props => {
const userId = window.localStorage.getItem("userId");
const dispatch = useDispatch();
const userData = useSelector(state=> state.authReducer[props.userData]);
const isAuthenticated = useSelector(state=> state.authReducer.isAuthenticated);
useEffect(()=> {
if(userId){
dispatch(userSetId(userId))
dispatch(userProfilFetch(userId))
}
}, [userId])
return(
<div>
<Router>
<Routes/>
</Router>
</div>
)
};

Related

How to save the set in State when using AppContext()?

I want to recive the state isAuthenticated in Home.js but when I change it before , in Login.js to true (I saw it in useEffect -->true) , the useAppContext() in Home.js return the old state -->false (the Old one in App.js) not the Last change in Login?
//----------------------in App.js----------------------
var [isAuthenticated, userHasAuthenticated] = useState('false');
<AppContext.Provider value={{ isAuthenticated, userHasAuthenticated }}>
//...
</AppContext.Provider>
//-------------- in Login.js---------------------
const { isAuthenticated, userHasAuthenticated } = useAppContext();
useEffect(() => {
console.log(isAuthenticated); // true
}, [isAuthenticated]);
const login = () => {
userHasAuthenticated('true');
window.location.assign("/Home");
}
// ---------------------in Home.js------------------------
const { isAuthenticated,userHasAuthenticated } = useAppContext();
console.log(isAuthenticated);// --->false ?

Next.js's per page layout component doesn't get value from Vercel's swr global configuration

The useSWR hook from swr works everywhere if I explicitly enter the fetcher.
const { data } = useSWR("http://...", fetcher);
However, if I used swr global configuration as shown below, the useSWR only works in First page but not in HeaderLayout component. I did some debugging and found out that in HeaderLayout doesn't receive the value from swr global configuration (SWRConfig in _app.tsx) even though it is wrapped inside.
I followed this doc https://nextjs.org/docs/basic-features/layouts#per-page-layouts for the page layout implementation
// _app.tsx
type NextPageWithLayout = NextPage & {
getLayout?: (page: React.ReactElement) => React.ReactNode;
};
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => page);
return (
<SWRConfig
value={{
fetcher: (resource, init) =>
fetch(resource, init).then((res) => res.json()),
}}
>
{getLayout(<Component {...pageProps} />)}
</SWRConfig>
);
}
// pages/first
const First = () => {
const [searchInput, setSearchInput] = useState("");
const router = useRouter();
const { data } = useSWR("http://...");
return (
<div>...Content...</div>
);
};
First.getLayout = HeaderLayout;
// layout/HeaderLayout
const HeaderLayout = (page: React.ReactElement) => {
const router = useRouter();
const { project: projectId, application: applicationId } = router.query;
const { data } = useSWR(`http://...`);
return (
<>
<Header />
{page}
</>
);
};
Helpful links:
https://nextjs.org/docs/basic-features/layouts#per-page-layouts
https://swr.vercel.app/docs/global-configuration
Next.js context provider wrapping App component with page specific layout component giving undefined data
Your First.getLayout property should be a function that accepts a page and returns that page wrapped by the HeaderLayout component.
First.getLayout = function getLayout(page) {
return (
<HeaderLayout>{page}</HeaderLayout>
)
}
The HeaderLayout is a React component, its first argument contains the props passed to it. You need to modify its signature slightly to match this.
const HeaderLayout = ({ children }) => {
const router = useRouter();
const { project: projectId, application: applicationId } = router.query;
const { data } = useSWR(`http://...`);
return (
<>
<Header />
{children}
</>
);
};
Layouts doesnt work if you declare Page as const. So instead of const First = () => {...} do function First() {...}

React Link changes URL on the browser but doesn't render new content

I am building a chat app and trying to match the id params to render each one on click.I have a RoomList component that maps over the rooms via an endpoint /rooms
I then have them linked to their corresponding ID. THe main components are Chatroom.js and RoomList is just the nav
import moment from 'moment';
import './App.scss';
import UserInfo from './components/UserInfo';
import RoomList from './components/RoomList';
import Chatroom from './components/Chatroom';
import SendMessage from './components/SendMessage';
import { Column, Row } from "simple-flexbox";
import { Route, Link, Switch } from 'react-router-dom'
function App() {
const timestamp = Date.now();
const timeFormatted = moment(timestamp).format('hh:mm');
const [username, setUsername] = useState('');
const [loggedin, setLoggedin] = useState(false);
const [rooms, setRooms] = useState([]);
const [roomId, setRoomId] = useState(0);
const handleSubmit = async e => {
e.preventDefault();
setUsername(username)
setLoggedin(true)
};
useEffect(() => {
let apiUrl= `http://localhost:8080/api/rooms/`;
const makeApiCall = async() => {
const res = await fetch(apiUrl);
const data = await res.json();
setRooms(data);
};
makeApiCall();
}, [])
const handleSend = (message) => {
const formattedMessage = { name: username, message, isMine: true};
}
return (
<div className="App">
<Route
path="/"
render={(routerProps) => (
(loggedin !== false) ?
<Row>
<Column>
{/*<Chatroom roomId={roomId} messages={messages} isMine={isMine}/>*/}
</Column>
</Row>
:
<form onSubmit={handleSubmit}>
<label htmlFor="username">Username: </label>
<input
type="text"
value={username}
placeholder="enter a username"
onChange={({ target }) => setUsername(target.value)}
/>
<button type="submit">Login</button>
</form>
)}
/>
<Switch>
<Route
exact
path="/:id"
render={(routerProps) => (
<Row>
<Column>
<UserInfo username={username} time={timeFormatted}/>
<RoomList rooms={rooms}/>
</Column>
<Column>
<Chatroom {...routerProps} roomId={roomId}/>
<SendMessage onSend={handleSend}/>
</Column>
</Row>
)}
/>
</Switch>
</div>
);
}
export default App;
RoomList.js
import { Row } from "simple-flexbox";
const RoomList = (props) => {
return (
<div className="RoomList">
<Row wrap="false">
{
props.rooms.map((room, index) => {
return (
<Link to={`/${room.id}`} key={index}>{room.id} {room.name}</Link>
)
})
}
</Row>
</div>
)
}
export default RoomList;
Chatroom.js
this is the main component that should render based on the ID
import Message from './Message';
import { Link } from 'react-router-dom'
const Chatroom = (props) => {
const [roomId, setRoomId] = useState(0);
const [name, setName] = useState('Roomname')
const [messages, setMessages] = useState([]);
useEffect(() => {
let apiUrl= `http://localhost:8080/api/rooms/`;
const id = props.match.params.id;
const url = `${apiUrl}${id}`;
const makeApiCall = async () => {
const res = await fetch(url);
const data = await res.json();
setRoomId(data.id);
setUsers(data.users)
setName(data.name)
};
makeApiCall();
}, []);
useEffect(() => {
const id = props.match.params.id;
const url = `http://localhost:8080/api/rooms/${id}/messages`;
const makeApiCall = async() => {
const res = await fetch(url);
const data = await res.json();
setMessages(data);
};
makeApiCall();
}, [])
return (
<div className="Chatroom">
{name}
</div>
)
}
export default Chatroom;```
when I click on the links I want the change to refresh the new content but it wont? any ideas why ? thank you in advance!
Notice that your functional component named App does not have any dependencies and that is fine since data should just be fetched once, on mount. However, on ChatRoom we want a new fetch everytime that roomId changes.
First thing we could do here is adding props.match.params.id directly into our initial state.
const [roomId, setRoomId] = useState(props.match.params.id); // set up your initial room id here.
Next we can add an effect that checks if roomId needs updating whenever props change. Like this:
useEffect(()=>{
if(roomId !== props.match.params.id) {
setRoomId(props.match.params.id)
}
}, [props])
Now we use roomId as our state for the api calls and add it in the brackets (making react aware that whenever roomId changes, it should run our effect again).
useEffect(() => {
let url = "http://localhost:8080/api/rooms/" + roomId; // add room id here
const makeApiCall = async () => {
const res = await fetch(url);
const data = await res.json();
setUsers(data.users)
setName(data.name)
};
makeApiCall();
}, [roomId]); // very important to add room id to your dependencies as well here.
useEffect(() => {
const url = `http://localhost:8080/api/rooms/${roomId}/messages`; // add room id here as well
const makeApiCall = async() => {
const res = await fetch(url);
const data = await res.json();
setMessages(data);
};
makeApiCall();
}, [roomId]) // very important to add room id to your dependencies as well here.
I believe that it should work. But let me build my answer upon this:
When mounted, meaning that this is the first time that the ChatRoom is rendered, it will go through your useEffect and fetch data using roomId as the initial state that we setup as props.match.params.id.
Without dependencies, he is done and would never fetch again. It would do it once and that's it. However, by adding the dependency, we advise react that it would watch out for roomId changes and if they do, it should trigger the function again. It is VERY IMPORTANT that every variable inside your useEffect is added to your brackets. There is eslint for it and it is very useful. Have a look at this post. It helped me a lot.
https://overreacted.io/a-complete-guide-to-useeffect/
Let me know if it works and ask me if there is still doubts. =)

On Redux update, how to prevent component from updating

I have a small component that renders another page, the webpage URL has a token attached as an URL parameter, like in the sample bellow:
const SampleComponent = () => {
const { refreshToken } = useSelector(state => state.auth);
const src = `${HOSTNAME}/page/?refresh_token=${refreshToken}`;
return <webview src={src} />;
};
export default SampleComponent;
I have a special cron that runs every hour and updates the tokens and Redux is updated as well with the new tokens.
window.tokensCron = new CronJob('0 0 * * *', () => {
store.dispatch(getTokens());
});
When the token is updated in Redux the page is being refreshed automatically.
How to prevent updating the component so that the refresh page won't happen?
So you want to use the token from redux state only when the component mounts?
You can make a custom hook that sets the token only once after the component mounts by deliberately leaving out a dependency of an effect, then use that in a HOC to pass the value of the token as it was when it mounted with other props to the component that needs the token:
//custom hook gets token only on mount
const useToken = () => {
const token = useSelector(selectToken);
const [val, setVal] = useState();
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => setVal(token), []);
return val;
};
//hoc that will only re render if props change (not when token changes)
const withToken = (Component) => (props) => {
const token = useToken();
const propsWithToken = useMemo(
() => ({ ...props, token }),
[props, token]
);
return token ? <Component {...propsWithToken} /> : null;
};
Make sure that the component you pass to withToken is a pure component so it won't get re rendered when props passed to it won't change.
Code snippet with this example is below.
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const {
useState,
useRef,
useEffect,
memo,
useMemo,
} = React;
const initialState = {
token: 1,
};
//action types
const REFRESH_TOKEN = 'REFRESH_TOKEN';
//action creators
const refreshToken = () => ({
type: REFRESH_TOKEN,
});
const reducer = (state = initialState, { type }) => {
if (type === REFRESH_TOKEN) {
return {
...state,
token: state.token + 1,
};
}
return state;
};
//selectors
const selectToken = (state) => state.token;
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (n) => (a) => n(a))
)
);
//custom hook gets token only on mount
const useToken = () => {
const token = useSelector(selectToken);
const [val, setVal] = useState();
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => setVal(token), []);
return val;
};
//hoc that will only re render if props change (not when token changes)
const withToken = (Component) => (props) => {
const token = useToken();
const propsWithToken = useMemo(
() => ({ ...props, token }),
[props, token]
);
return token ? <Component {...propsWithToken} /> : null;
};
const Component = ({ token }) => {
const r = useRef(0);
r.current++;
return (
<div>
rendered: {r.current} token: {token}
</div>
);
};
//using React.memo to make Component a pure component
const PureWithToken = withToken(memo(Component));
const App = () => {
const token = useSelector(selectToken);
const [toggle, setToggle] = useState(true);
const dispatch = useDispatch();
//refresh token every second
useEffect(() => {
const interval = setInterval(
() => dispatch(refreshToken()),
1000
);
return () => clearInterval(interval);
}, [dispatch]);
return (
<div>
<div>token:{token}</div>
<label>
Toggle component with token
<input
type="checkbox"
checked={toggle}
onChange={() => setToggle((t) => !t)}
/>
</label>
{/* when component re mounts it will have the newest token */}
{toggle ? <PureWithToken /> : null}
</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>
<div id="root"></div>
Your state may be malformed.
As I see you have:
a token that updates frequently
an initial token, which is the first token value, and never change
So consider modify your state to follow this structure:
state = {
auth: {
initialToken,
refreshToken
}
};
Then in your component, simply do that:
const initialToken = useSelector(state => state.auth.initialToken);
Important, in your useSelector please returns only the value you want (your token, not the whole auth). Like that your component will update ONLY if your token changes.
As you do in your current code if auth changes, your component is updated even if token did not change.

Reducer is not saving data to state in React Context API

I am implementing authentication functionality in my app and when I try to save auth token which I get from my backend to reducer state it does nothing... I am new to this so there may be some dumb error.
This is my store.js file:
import React from 'react';
export const initialState = { access_token: null };
export const reducer = (state, action) => {
switch (action.type) {
case "SET_TOKEN":
console.log(action.data) // this does return the token which means data is passed correctly
return { access_token: action.data };
case "REMOVE_TOKEN":
return { access_token: null };
default:
return initialState;
}
};
export const Context = React.createContext();
This is my root component file AppRouter.js:
function AppRouter() {
const [store, dispatch] = useReducer(reducer, initialState);
const access_token = store.access_token;
console.log(access_token);
const AuthenticatedRoute = GuardedRoute(access_token);
return (
<Context.Provider value={{store, dispatch}}>
<Router>
<Switch>
<Route exact path="/" component={HomeScreen}/>
<Route exact path="/register" component={RegisterScreen}/>
<Route exact path="/login" component={LoginScreen}/>
<AuthenticatedRoute component={DashboardScreen} exact path={"/dashboard"}/>
</Switch>
</Router>
</Context.Provider>
)
}
So to me all this looks fine, and then this is the _login function in which I send the dispatch() to save the token(EDIT: this is everything between start of component function and return():
const [afterSuccessRegister, setAfterSuccessRegister] = useState(false);
const [emailInput, setEmailInput] = useState("");
const [passwordInput, setPasswordInput] = useState("");
const [loginErrorMessage, setLoginErrorMessage] = useState("");
const [createdUserEmail, setCreatedUserEmail] = useState("");
const { store, dispatch } = useContext(Context);
const _login = () => {
axios.post(`${ROOT_API}/v1/users/login`, {
"user": {
"email": emailInput,
"password": passwordInput
}
}, {}).then(res => {
console.log(res.data);
dispatch({type: 'SET_TOKEN', data: res.data.meta.access_token});
}).catch(err => {
console.log(err.response);
setLoginErrorMessage(err.response.data.message)
})
};
const _handleEmailChange = (e) => {
setEmailInput(e.target.value);
};
const _handlePasswordChange = (e) => {
setPasswordInput(e.target.value);
};
useEffect(() => {
if(typeof props.location.state !== "undefined") {
if (typeof props.location.state.success_register === 'undefined' || props.location.state.success_register === null || props.location.state.success_register === false) {
console.log("login");
} else {
setAfterSuccessRegister(true);
setCreatedUserEmail(props.location.state.created_user_email);
delete props.location.state;
}
}
}, [props.location.state]);
I really don't know why is it not saving it even though data is passed correctly. I tried adding console.log(store.access_token) after my login request has finished to see if it was saved, but it returns null.
Thanks!

Categories