I'm building an app with guarded actions and would like to achieve the following flow:
anonymous user tries to run guarded action (e.g. play video - essentially modify React state)
login modal pops up without any redirects
user fills in the credentials and hits login button
any kind of loader shows up (and replaces the previous app tree) and stays on the screen as long as user is being logged in
once logged in, loader disappears and the authenticated app renders
video starts playing, because the app knows the action user wanted to take
I haven't found a way to do that, because replacing the old app tree with loader and then putting it back on the screen makes the action run on an already unmnounted component.
I'm using a <UserProvider> component that wraps the whole tree to provide it with authenticated state.
Codesandbox to illustrate the issue:
https://codesandbox.io/s/react-context-callback-l9xe9?file=/src/App.js
const MyContext = React.createContext();
const MyProvider = ({ children }) => {
const [auth, setAuth] = React.useState(false);
const [reset, setReset] = React.useState(false);
const [callback, setCallback] = React.useState(null);
const guard = (cb) => {
auth ? cb() : setCallback(() => cb);
};
React.useEffect(() => {
if (auth) {
setReset(true);
setTimeout(() => {
setReset(false);
}, 1000);
}
}, [auth]);
if (reset) {
return <p style={{ color: "red" }}>I pretend I'm logging you in</p>;
}
return (
<MyContext.Provider
value={{
setAuth,
auth,
callback,
setCallback,
guard
}}
>
{children}
</MyContext.Provider>
);
};
const Child = () => {
const [count, setCount] = React.useState(0);
const mounted = React.useRef(false);
React.useEffect(() => {
mounted.current = true;
return () => {
mounted.current = false;
};
}, []);
const myCallback = () => {
setCount((cnt) => cnt + 1);
};
const { auth, setAuth, guard, callback, setCallback } = React.useContext(
MyContext
);
return (
<div>
logged in: {`${auth}`}
<br />
count: {count}
<br />
{callback && (
<button
onClick={() => {
setAuth(true);
callback();
setCallback(null);
}}
>
pretend login
</button>
)}
<button onClick={() => guard(myCallback)}>
increment (when authenticated)
</button>
</div>
);
};
Is that even possible?
That's a use case for lifting the state up, you can't preserve the state after the component unmounts (Of course you can try reading from local storage but it's essentially the same logic as lifting the state).
Therefore lift the count state (to App or to the provider itself):
export default function App() {
const state = React.useState(0);
...
Related
I am using this react library https://github.com/gregberge/loadable-components to load a Component with Ref to access instance values using useImperativeHandle but ref is always null.
Here is my code
import loadable from '#loadable/component';
export default function ParentComponent(props){
const currentPageRef = useRef();
const[currentPage,setCurrentPage]=useState();
const loadPage= (page= 'home') => {
const CurrentPage = loadable(() => import(`./${page}`));
return (
<CurrentPage
ref={currentPageRef}
/>
);
}
useEffect(() => {
console.log(currentPageRef); //This is always logging current(null);
let pageTitle= currentPageRef.current?.getTitle();
let pageSubTitle= currentPageRef.current?.getSubTitle();
console.log(` Page Title=${pageTitle}`); //This is always coming back as null
console.log(`Page SubTitle=${pageSubTitle}`); //This is also always coming back as null
}, [currentPage]);
return (
<div>
<button onClick={() => {
setCurrentPage(loadPage('youtube));
}}>
LoadPage
</button>
</div>
);
}
Where each of the child components contains a useImperativeHandle to expose instance functions but I can't seem to access any of the functions because currentPageRef is always null
Here is an example of one of the child pages that contains the useImperativeHandle implementation
const YouTubePage= React.forwardRef((props,ref)=>{
const [connected, setConnected] = useState(false);
const getTitle = () => {
return connected ? "Your YouTube Channels" : "YouTube";
}
const getSubTitle = () => {
return connected ? "Publishable content is pushed to only connected channels. You may connect or disconnect channel(s) as appropriate" : "Connect a YouTube account to start posting";
}
useImperativeHandle(ref, () => ({ getTitle, getSubTitle }));
return (<div></div>);
});
Any ideas as to why that might be happening?
Thank you
From your code example your aren't actually rendering the component which you set by the state setter:
export default function ParentComponent(props) {
//...
// Render the page
return (
<>
{currentPage}
<div>...</div>
</>
);
}
I am new to React and I tried to toggle the Login/Logout based on the current state of authentication. I've used Google OAuth to perform the authentication.
I have a state variable to say if the user is authenticated or not and is defaulted to false. Upon successful authentication, I set the state to true.
Now the problem is, after completing a successful authentication, when I refresh the screen, the screen reloads and I see the console.log printing false and login appears momentarily. And after a second the console.log prints true and then the logout appears. How do I avoid showing login screen (for that one second after refreshing the screen) when the authentication is completed? Can someone help me please? Thanks.
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
setIsAuthenticated(false)
}, [])
const handleSuccessAuth = x => {
setIsAuthenticated(true)
}
const handleFailureAuth = x => {
setIsAuthenticated(false)
}
const handleLogout = x => {
setIsAuthenticated(false)
}
console.log(isAuthenticated)
if(!isAuthenticated)
{
return (
<div>
<LoginView
handleSuccessAuth = {handleSuccessAuth}
handleFailureAuth = {handleFailureAuth}
/>
</div>
)
}
else
{
return (
<div>
<LogoutView
handleLogout = {handleLogout}
/>
</div>)
}
If your variable goes from false to true it means your code is doing something, probably an AJAX call, my recommendation is to show a loading scree/message until the AJAX request is completed.
There is no way to keep the variable intact on reload but you can keep a variable that tracks if the user authentication has been initialized and show a loading indicator in the meantime
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [authLoaded, setAuthLoaded] = useState(false)
// this does nothing, passing false to useState above sets the initial value
// useEffect(() => {
// setIsAuthenticated(false)
// }, [])
const handleSuccessAuth = x => {
setIsAuthenticated(true)
setAuthLoaded(true)
}
const handleFailureAuth = x => {
setIsAuthenticated(false)
setAuthLoaded(true)
}
const handleLogout = x => {
setIsAuthenticated(false)
}
console.log(isAuthenticated)
if (!authLoaded) {
return <div>loading...</div>
}
if(!isAuthenticated)
{
return (
<div>
<LoginView
handleSuccessAuth = {handleSuccessAuth}
handleFailureAuth = {handleFailureAuth}
/>
</div>
)
}
else
{
return (
<div>
<LogoutView
handleLogout = {handleLogout}
/>
</div>)
}
I believe olivier-boisse alluded to using localStorage to persist your state. You can use an useEffect hook to persist your isAuthenticated to localStorage when the value updates, and use a state initializer function to read in the initial state from local storage.
const [isAuthenticated, setIsAuthenticated] = useState(() => {
return !!JSON.parse(localStorage.getItem('auth');
});
useEffect(() => {
localStorage.setItem('auth', JSON.stringify(isAuthenticated));
}, [isAuthenticated]);
I have (what seems to be) a very peculiar situation where I seem to be getting extra events emitted based on my Redux state.
I have narrowed the behavior down to whether or not I make a successful request to my /users endpoint and retrieve a list of users which is then stored in Redux.
If the commented code is not active (as it is currently shown), I am able to successfully render the modal(s) reliably and step between states.
If the commented code is active, the (which is what is behind the as well) emits an onDismiss call immediately. This has the result of closing the modal immediately.
If the commented code is active, but the response from the thunk is a 401 and the user data is not loaded (i.e., the state of the user key in redux is a failure, not success, then the modal works -- though of course, there are no users to select.
I have confirmed this behavior is consistent no matter where I seem to make this fetch request (initially it was in the App.tsx to be called immediately. I also tried it in an intermediate component).
Question(s):
Can you explain why I might be getting different behavior in my click handlers based on what is in my state?
Is there something I'm missing and I'm conflating my Redux state with the actual behavior?
I know I can solve this by adding a event.stopPropagation() call in strategic places (e.g., on the first button that opens the <ConfirmationBox> and then again on the button in the <ConfirmationBox> that transitions to the SelectUser modal), but are there other solutions?
//pinFlow.tsx
type States =
| { state: 'Confirm' }
| { state: 'SelectUser' }
| { state: 'SubmitPin'; user: User };
export function pinFlow<T extends ConfirmationBoxProps>(
ConfirmationBox: React.FC<T>,
authorization: Authorization,
) {
const [state, setState] = React.useState<States>({ state: 'Confirm' });
// const dispatch=useDispatch();
// initialize users
// const users = useSelector((state: InitialState) => state.pinAuth.users);
// const fetchUsers = useCallback(() => {
// dispatch(fetchUsersThunk());
// }, [dispatch]);
// useEffect(() => {
// if (users.state === RemoteDataState.NotStarted) {
// fetchUsers();
// }
// }, [fetchUsers, users.state]);
return (props: T) => {
const users = useSelector((state: InitialState) =>
mapRemoteData(state.pinAuth.users, users =>
users.filter(user => user.authorizations.includes(authorization)),
),
);
switch (state.state) {
case 'Confirm': {
return (
<ConfirmationBox
{...props}
onSubmit={(_event: React.MouseEvent) => {
setState({ state: 'SelectUser' });
}}
/>
);
}
case 'SelectUser': {
return (
<Modal
title={'PIN Required'}
canClickOutsideToDismiss={true}
onDismiss={() => {
setState({ state: 'Confirm' });
}}
>
<p className={style.selectProfileText}>Select your profile:</p>
<pre>
<code>{JSON.stringify(users, null, 4)}</code>
</pre>
{/*
<UserList users={users.data} /> */}
</Modal>
);
}
default: {
return <Modal title="others">all others</Modal>;
}
}
};
}
The code is used in another component like so:
function Comp(){
const [selected, setSelected] = useState();
const [mode, setMode] = useState();
const ConfirmationModal =
protected
? pinFlow(MenuItemModal, permission)
: MenuItemModal;
return(
<ConfirmationModal
item={selected}
mode={mode}
disabled={availability.state === RemoteDataState.Loading}
errorMessage={tryGetError(availability)}
onCancel={() => {
setMode(undefined);
dispatch(resetAvailability());
}}
onSubmit={(accessToken: string) => {
dispatch(findAction(selected, mode, accessToken));
}}
/>
)
}
I'm using react-hook-form library with a multi-step-form
I tried getValues() in useEffect to update a state while changing tab ( without submit ) and it returned {}
useEffect(() => {
return () => {
const values = getValues();
setCount(values.count);
};
}, []);
It worked in next js dev, but returns {} in production
codesandbox Link : https://codesandbox.io/s/quirky-colden-tc5ft?file=/src/App.js
Details:
The form requirement is to switch between tabs and change different parameters
and finally display results in a results tab. user can toggle between any tab and check back result tab anytime.
Implementation Example :
I used context provider and custom hook to wrap setting data state.
const SomeContext = createContext();
const useSome = () => {
return useContext(SomeContext);
};
const SomeProvider = ({ children }) => {
const [count, setCount] = useState(0);
const values = {
setCount,
count
};
return <SomeContext.Provider value={values}>{children}</SomeContext.Provider>;
};
Wrote form component like this ( each tab is a form ) and wrote the logic to update state upon componentWillUnmount.
as i found it working in next dev, i deployed it
const FormComponent = () => {
const { count, setCount } = useSome();
const { register, getValues } = useForm({
defaultValues: { count }
});
useEffect(() => {
return () => {
const values = getValues(); // returns {} in production
setCount(values.count);
};
}, []);
return (
<form>
<input type="number" name={count} ref={register} />
</form>
);
};
const DisplayComponent = () => {
const { count } = useSome();
return <div>{count}</div>;
};
Finally a tab switching component & tab switch logic within ( simplified below )
const App = () => {
const [edit, setEdit] = useState(true);
return (
<SomeProvider>
<div
onClick={() => {
setEdit(!edit);
}}
>
Click to {edit ? "Display" : "Edit"}
</div>
{edit ? <FormComponent /> : <DisplayComponent />}
</SomeProvider>
);
}
I have a custom hook that keeps a list of toggle states and while I'm seeing the internal state aligning with my expectations, I'm wondering why a component that listens to changes on the state kept by this hook isn't re-rendering on change. The code is as follows
const useToggle = () => {
const reducer = (state, action) => ({...state, ...action});
const [toggled, dispatch] = useReducer(reducer, {});
const setToggle = i => {
let newVal;
if (toggled[i] == null) {
newVal = true;
} else {
newVal = !toggled[i];
}
dispatch({...toggled, [i]: newVal});
console.log('updated toggled state ...', toggled);
};
return {toggled, setToggle};
};
const Boxes = () => {
const {setToggle} = useToggle()
return Array.from({length: 8}, el => null).map((el,i) =>
<input type="checkbox" onClick={() => setToggle(i)}/>)
}
function App() {
const {toggled} = useToggle()
const memoized = useMemo(() => toggled, [toggled])
useEffect(() => {
console.log('toggled state is >>>', toggled) // am not seeing this on console after changes to toggled
}, [toggled])
return (
<div className="App">
<Boxes />
</div>
);
}
It's because you are using useToggle twice.
once in the App
another one in the Boxes.
When you dispatch the action in Boxes, it's updating the toggled instance for Boxes (which is not retrieved in it).
Think of your custom hook like how you use useState. When you use useState, each component gets its own state. Same goes for the custom hook.
So there are a few ways you can address the issue.
Pass the setToggle from App to Boxes via prop-drilling
Use Context API (or Redux or other statement management library to pass
setToggle instance in the App component down)
Here is an example of prop-drilling.
You can follow along
const Boxes = ({ setToggle }) => {
// const { setToggle } = useToggle();
return Array.from({ length: 8 }, el => null).map((el, i) => (
<input key={i} type="checkbox" onClick={() => setToggle(i)} />
));
};
function App() {
const { toggled, setToggle } = useToggle();
useEffect(() => {
console.log("toggled state is >>>", toggled); // am not seeing this on console after changes to toggled
}, [toggled]);
return (
<div className="App">
<Boxes setToggle={setToggle} />
</div>
);
}
Note: that I added key props in Boxes using the index i(and it is a bad practice by the way)
You can see that it's now working as you'd expect.