If user is logged in, render the component. If not, render login page. I notice, however, that this function is called twice. The first time, useAuthDataContext() is null. The second time, I get the correct object back.
const PrivateRoute = ({ component, ...options }) => {
const { userData } = useAuthDataContext()
console.log(userData)
const finalComponent = userData != null ? component : Login
return (
<Route {...options} component={finalComponent} />
)
};
export default PrivateRoute
I have rewritten this function as follows. Here, PrivateRoute2 is called only once, and useAuthDataContext() returns null.
const PrivateRoute2 = ({ component: Component, ...rest }) => {
const { userData } = useAuthDataContext()
console.log(userData)
return (
<Route
{...rest}
render={props =>
userData != null ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)
}
/>
)
}
Here is my useAuthDataContext() implementation that is causing the rerender:
export const AuthDataContext = createContext(null)
const initialAuthData = {}
const AuthDataProvider = props => {
const [authData, setAuthData] = useState(initialAuthData)
useLayoutEffect( (props) => {
const getUser = async () => {
try {
const userData = await authService.isAuthenticated()
setAuthData( {userData})
} catch (err) {
setAuthData({})
}
}
getUser()
}, [])
const onLogout = () => {
setAuthData(initialAuthData)
}
const onLogin = newAuthData => {
const userData = newAuthData
setAuthData( {userData} )
}
const authDataValue = useMemo(() => ({ ...authData, onLogin, onLogout }), [authData])
return <AuthDataContext.Provider value={authDataValue} {...props} />
}
export const useAuthDataContext = () => useContext(AuthDataContext)
export default AuthDataProvider
I think i found one solution. See this post https://hackernoon.com/whats-the-right-way-to-fetch-data-in-react-hooks-a-deep-dive-2jc13230
Related
export const LoginContext = React.createContext();
export const DetailsContext = React.createContext();
function App() {
const username = localStorage.getItem("bankDetails");
const [userDetails, setUserDetails] = useState({});
const [isValid, setisValid] = useState(false);
useEffect(() => {
if (username !== null) {
Axios.post("http://localhost:3001/userDetails", {
username: username,
}).then((res) => {
if (res.data.err) {
console.log("err");
} else {
setUserDetails(res.data.details[0]);
setisValid(true);
}
});
}
}, []);
return (
<LoginContext.Provider value={{ isValid, setisValid }}>
<DetailsContext.Provider value={{ userDetails, setUserDetails }}>
<Router>
<Routes>
<Route ... />
</Routes>
</Router>
</DetailsContext.Provider>
</LoginContext.Provider>
);
}
export default App;
Transactions.js
function Transactions() {
const { isValid } = useContext(LoginContext);
const { userDetails, setUserDetails } = useContext(DetailsContext);
const [allDetails, setAllDetails] = useState([]);
const [transactions, setTransactions] = useState([]);
useEffect(() => {
console.log(userDetails);
Axios.get("http://localhost:3001/transactTo").then((rest) => {
setAllDetails(rest.data);
});
// setTransactions(JSON.parse(userDetails.transactions));
}, [userDetails]);
return isValid ? <h1>Valid</h1> : <h1>Not Valid</h1>
}
export default Transactions;
The userDetails logs an empty object first and data object after re-render but after uncommenting the setTransactions(JSON.parse(userDetails.transactions)) part it only logs an empty object and then an error stating: Unexpected token u in JSON at position 0. It only happens on page refresh and not when I navigate from another page.
Also tried adding second effect but it didn't helped:
useEffect(() => {
setTransactions(JSON.parse(userDetails.transactions));
}, [allDetails]);
It is an empty object because API requests are asynchronous. It is a normal thing.
Unexpected token u in JSON at position 0 this means that userDetails.transactions isn't a json. It's probably undefined that's why u
useEffect(() => {
// try returning when the property is `undefined`
if (!userDetails.transations) return;
setTransactions(JSON.parse(userDetails.transactions));
}, [allDetails]);
I am trying to use getStaticProps for my Layout component as described here, but do struggle to solve this for my specific case:
_app.tsx
const NoCheck: React.FC = ({ children }) => <>{children}</>
function App({ Component, pageProps }: AppProps) {
const neoPage = Component as NeoPage
const LayoutComponent = neoPage.Layout || Layout
const { withAuthCheck = true } = neoPage
const CheckAuthComponent = withAuthCheck ? CheckAuth : NoCheck
return (
<CommonProviders pageProps={pageProps} overmindConfig={config}>
<CheckAuthComponent>
<LayoutComponent>
<Component {...pageProps} />
</LayoutComponent>
</CheckAuthComponent>
</CommonProviders>
)
}
export default App
LayoutUnauthorized.tsx
export const LayoutUnauthorized: React.FC<Props> = ({
children,
systemNormal,
}) => {
return (
<Flex>
<SystemStatus systemNormal={systemNormal} />
{children}
</Flex>
)
}
Login.tsx (in /page)
export const Login: NeoPage = () => {
return (
...
)
}
Login.Layout = LayoutUnauthorized
Login.withAuthCheck = false
export async function getStaticProps() {
const res = await fetch("https://company.com")
const systemNormal = await res.ok
return {
props: {
systemNormal,
},
revalidate: 1,
}
}
So Layout has a property I would like getStaticProps to pass. How can this be achieved?
In my parent component, Dashboard.tsx, I have a child component, Expenses.tsx, that makes an API fetch call and then displays the data. The parent component has a Router that allows you to navigate to different URL's in the parent component, which forces everything to re-render every time you navigate to a new path or render a new child component. How can I make it so that this fetch call is only made one time? I've tried using the useRef() hook but it re-initializes every time there is a re-render and I have the same problem.
Here is Dashboard.tsx:
export const Dashboard = () => {
const d = new Date()
const history = useHistory()
const { user, setUser } = useAuth()
const [categories, setCategories] = useState({
expenseCategories: [],
incomeCategories: []
})
const getCategories = async(user_id: number) => {
await fetch(`/api/getCategories?user_id=${user_id}`)
.then(result => result.json())
.then(result => setCategories(result))
}
useEffect(() => {
if (user.info.user_id) {
getCategories(user.info.user_id)
}
}, [])
const dashboardItems = [
{
value: 'Add Expense',
path: '/dashboard/addExpense'
},
{
value: 'Add Income',
path: '/dashboard/addIncome'
},
{
value: 'Logout',
path: '/login',
onClick : async() => {
localStorage.clear()
setUser({
info: {
user_id: null,
email: null,
username: null
},
token: null
})
},
float: 'ml-auto'
}
]
return(
<div>
<DashboardNavbar items={dashboardItems}/>
<div className="wrapper">
<p>{`Hello, ${user.info.username}!`}</p>
<DateAndTime />
<Expenses date={d}/>
<Income date={d}/>
<Switch>
<Route path='/dashboard/addExpense'>
<AddItemForm user={user} type={'expenses'} categories={categories.expenseCategories} />
</Route>
<Route path='/dashboard/addIncome'>
<AddItemForm user={user} type={'income'} categories={categories.incomeCategories} />
</Route>
</Switch>
<Logout />
</div>
</div>
)
}
And here is Expenses.tsx, where the fetch call is being made:
export const Expenses = (props: ExpensesProps) => {
const [isLoading, setIsLoading] = useState(true)
const { date } = props
const { user } = useAuth()
const m = date.getMonth() + 1
const s = '0'.concat(m.toString())
const [total, setTotal] = useState<number>(0)
useEffect(() => {
const getTotalExpenses = async() => {
await fetch(`/api/expenses?user_id=${user.info.user_id}&month=${s}`)
.then(response => response.json())
.then(result => {
if (result) {
setTotal(parseFloat(result))
}
})
.then(result => {
setIsLoading(false)
})
}
if (user.info.user_id) {
getTotalExpenses()
}
}, [])
return isLoading ? (
<div>
loading...
</div>
) : (
<div>
{`Your monthly expenses so far are: $${total}.`}
</div>
)
}
i want to access isLoading state ineach component of the main component. basically when load() in useAnother hook starts and ends i set loading state to true and false.
below is my code without context provider,
function useAnother(Id: string) {
const [compId, setCompId] = React.useState(undefined);
const [isLoading, setIsLoading] = React.useState(false);
const comp = useCurrentComp(Id);
const load = useLoad();
if (comp && comp.id !== compId) {
setCompId(comp.id);
const prevCompId = compId !== undefined;
if (prevCompId) {
setIsLoading(true);
load().then(() => {
setIsLoading(false);
});
}
}
}
function Main ({user}: Props) {
useAnother(user.id); //fetching isLoading here from useHook
return (
<Wrapper>
<React.suspense>
<Switch>
<Route
path="/"
render={routeProps => (
<FirstComp {...routeProps} />
)}
/>
<Route
path="/items"
render={routeProps => (
<SecondComp {...routeProps} />
)}
/>
//many other routes like these
</Switch>
</React.suspense>
</Wrapper>
);
}
Now with using context provider
interface LoadingContextState {
isLoading: boolean;
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
}
const initialLoadingState: LoadingContextState = {
isLoading: false, setIsLoading: () => {},
};
export const LoadingContext = React.createContext<LoadingContextState>(
initialLoadingState
);
export const LoadingContextProvider: React.FC = ({ children }) => {
const [isLoading, setIsLoading] = React.useState<boolean>(false);
return (
<LoadingContext.Provider
value={{
isLoading,
setIsLoading,
}}
>
{children}
</LoadingContext.Provider>
);
};
function App() {
return (
<LoadingContextProvider>
<Main/>
</LoadingContextProvider>
);
}
function useAnother(Id: string) {
const [compId, setCompId] = React.useState(undefined);
const {setIsLoading} = React.useContext(LoadingContext);
const comp = useCurrentComp(Id);
const load = useLoad();
if (comp && comp.id !== compId) {
setCompId(comp.id);
const prevCompId = compId !== undefined;
if (prevCompId) {
setIsLoading(true);
load().then(() => {
setIsLoading(false);
});
}
}
}
function Main ({user}: Props) {
useAnother(user.id);
return (
<Wrapper>
<React.suspense>
<Switch>
<Route
path="/"
render={routeProps => (
<FirstComp {...routeProps} />
)}
/>
<Route
path="/items"
render={routeProps => (
<SecondComp {...routeProps} />
)}
/>
//many other routes like these
</Switch>
</React.suspense>
</Wrapper>
);
}
function FirstComponent () {
const {isLoading} = React.useContext(LoadingContext);
return (
<Wrapper isLoading={isLoading}/>
);
}
this works. but i dont want to use context provider instead is it possible to use hook instead of context for this.
could someone help me with this. thanks.
}
Now with using context provider
interface LoadingContextState {
isLoading: boolean;
setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
}
const initialLoadingState: LoadingContextState = {
isLoading: false, setIsLoading: () => {},
};
export const LoadingContext = React.createContext<LoadingContextState>(
initialLoadingState
);
This is possible if you choose to return the value of isLoading from useAnother hook.
function useAnother(Id: string) {
const [compId, setCompId] = React.useState(undefined);
const [isLoading, setIsLoading] = React.useState(false);
const comp = useCurrentComp(Id);
const load = useLoad();
if (comp && comp.id !== compId) {
setCompId(comp.id);
const prevCompId = compId !== undefined;
if (prevCompId) {
setIsLoading(true);
load().then(() => {
setIsLoading(false);
});
}
}
return isLoading; // return the isLoading value from the hook
}
When you call useAnother hook in the Main component you can get the isLoading value and pass it as props to the children of Main component.
For example,
const isLoading = useAnother(user.id)
// when you render FirstComp pass isLoading as prop also, the FirstComp
// needs to have appropriate code for handling the `isLoading` value
<FirstComp {...routeProps} isLoading={isLoading} />
The issue with this approach, suppose you have another component which is a child of the FirstComponent and it also needs the isLoading value. To provide the isLoading value you have to pass isLoading drilling the prop through several components in the hierarchy which is an anti-pattern.
That is why I suggest keep using the context API approach,
Using contexts can generate some complex boilerplate code when using typescript, but it saves you from drilling the prop in the component hierarchy.
I have contexts/RoomContext.tsx:
import { useState, createContext } from 'react';
const RoomContext = createContext([{}, () => {}]);
const RoomProvider = (props) => {
const [roomState, setRoomState] = useState({ meetingSession: null, meetingResponse: {}, attendeeResponse: {} })
return <RoomContext.Provider value={[roomState, setRoomState]}>
{props.children}
</RoomContext.Provider>
}
export { RoomContext, RoomProvider }
Then in my component, RoomPage.tsx, I have:
const RoomPageComponent = (props) => {
const router = useRouter()
const [roomState, setRoomState] = useContext(RoomContext);
useEffect(() => {
const createRoom = async () => {
const roomRes = await axios.post('http://localhost:3001/live')
console.log('roomRes', roomRes)
setRoomState(state => ({ ...state, ...roomRes.data }))
}
if (router.query?.id) {
createRoom()
}
}, [router])
return <RoomPageWeb {...props} />
}
export default function RoomPage(props) {
return (
<RoomProvider>
<RoomPageComponent {...props} />
</RoomProvider>
)
}
But I get a complaint about the setRoomState:
This expression is not callable.
Type '{}' has no call signatures.
The issue here is that you are trying to use RoomContext in a component(RoomPage) which doesn't have RoomContext.Provider, higher up in the hierarchy since it is rendered within the component.
The solution here to wrap RoomPage with RoomProvider
import { RoomProvider, RoomContext } from '../../contexts/RoomContext'
function RoomPage(props) {
const [roomState, setRoomState] = useContext(RoomContext);
useEffect(() => {
const createRoom = async () => {
const roomRes = await axios.post('http://localhost:3001/live')
console.log('roomRes', roomRes)
setRoomState(state => ({...state, ...roomRes.data}))
}
...
return (
<RoomPageWeb {...props} />
)
export default (props) => (
<RoomProvider><RoomPage {...props} /></RoomProvider>
)