I am having an issue where it seems my state variable is being overwritten by the default/initial value I set when calling useState(). I have const [token, setToken] = useState(""). I pass in the setToken function to my Login component, and when I log in I update the token via the setToken function.
In my App.js I only want to render the Login component if the token is not set, otherwise, I redirect to a Post.js component. Once I log in my token is being set and I am being redirected to my Posts.js page, the problem is if I refresh the page the token is overwritten again to the initial value and I'm not sure why that is.
My understanding is that useState() is only called on the initial render, otherwise the state is obtained from whatever is stored in state which since I called the setToken function, should be the state with the token, not an empty string.
What am I missing?
App.js
function App() {
const [errors, setErrors] = useState([]);
const [token, setToken] = useState("");
console.log('token', token)
return (
<Fragment>
<Router>
<Navbar />
<Route exact path="/" component={Landing} />
<Container>
{!errors.length ? <></> : <Alerts errors={errors} handleError={setErrors}/> }
<Switch>
<Route
exact path="/login"
render={ token ? (props) => <Redirect push to="/posts" {...props} /> : (props) => (<Login error={setErrors} isLoggedIn={setToken} {...props} />)}
/>
<Route exact path="/posts"
render={token ? (props) => <Posts token={token} {...props} /> : (props) => <Redirect push to="/login" {...props} />} />
</Switch>
</Container>
</Router>
</Fragment>
);
}
Pertinent section of Landing.js
const handleSubmit = async (e) => {
e.preventDefault();
const res = await login(email, password);
if(res.status !== 200){
props.error(res.data.errors);
}else{
props.error([])
props.isLoggedIn(res.data.token)
}
}
UPDATE:
I did figure out how to do what I wanted to do, by storing my token in local storage. In my App.js I'm doing this now
function App() {
const [errors, setErrors] = useState([]);
const [token, setToken] = useState(null);
useEffect(() => {
setToken(window.localStorage.getItem("token"));
})
This is not overwriting my token state as it was initially, so it's doing what I want, but I'm still wondering what I was doing wrong in my original implementation?
when you refresh the page your app runs from the beginning. If you don't want to lose your token on page refresh you should save/retrieve your to/from localStorage.
Related
I'm building a rest API with auth implementation using Django-rest-framework and Django-rest-knox, And in the front-end I'm using React.js. I want to store the auth token securely in the front-end and I know that the best way to do it is using httponly cookies so I've used this in my code:
django-rest-knox with cookies
from django.contrib.auth import login
from rest_framework import permissions
from rest_framework.authtoken.serializers import AuthTokenSerializer
from knox.views import LoginView as KnoxLoginView
class LoginView(KnoxLoginView):
permission_classes = (permissions.AllowAny,)
def post(self, request, format=None):
serializer = AuthTokenSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.validated_data['user']
login(request, user)
response = super(LoginView, self).post(request, format=None)
token = response.data['token']
del response.data['token']
response.set_cookie(
'auth_token',
token,
httponly=True,
samesite='strict'
)
return response
This code works perfectly so it send a Set-Cookie header and create an httponly cookie. But the problem is that if those cookies aren't accessible with JavaScript how can I access protected routes from react and axios using the Authorization header.
Create a higher order component as "ProtectedRoute" that renders component only if user is authenticated and authorized.
ProtectedRoute Component
const ProtectedRoute = ({component:Component, ...rest}) => {
const [isLoggedIn, setLoggedIn] = useState({loggedIn:'', loaded:false})
useEffect(async () => {
const userStatus = await validateUserLoggedIn();
setLoggedIn((prevState) => {return {loggedIn:userStatus, loaded:true}})
}, [])
return(
<Route {...rest} render={(routerProps) => {
if(isLoggedIn.loaded){
if(isLoggedIn.loggedIn ==='success'){
return(
<Component {...rest} {...routerProps}/>
)
}else{
return(
<Redirect to={{
pathname:'/login',
state:{from : routerProps.location}
}} />
)
}
}else{
return(
<Row style={{display: 'flex', justifyContent: 'center'}}>
(<CustomLoader />)
</Row>
)
}
}}
/>
)
}
<ProtectedRoute exact path="/admin" component={Admin} />
ProtectedRoute is a HOC component that returns the component based on "validateUserLoggedIn" return value. Refer "react-router-dom" for more info. Syntax to create HOC for routes is as below
psuedo code
const ProtectedRoute({component:Component, ...rest}) => {
return (
<Route {...rest} render={
(routerProps) => {
isUserAuthenticated ?
(<Component {...rest} {...routerProps} />) :
"redirect to login or display unauthorized"
}
}
)
}
validateUserLoggedIn function makes an API call using the credentials stored in HTTP only cookies and if request is forbidden, it will make a call to an refresh end-point to get new access token and make a request again to verify is user authenticated.
ValidateuserLoggedIn
const validateUserLoggedIn = async () => {
const loggedIn = {status:''}
const axiosValidateUserLoggedIn = axios.create(VALIDATEUSERAXIOSCONFIG);
axiosValidateUserLoggedIn.interceptors.response.use(
(resp) => {return resp},
(err) => {
const originalRequest = err.config;
const refreshTokenStatus = refreshToken();
if(refreshTokenStatus === 'success'){
axios(originalRequest)
}else{
window.location.href = '/login';
}
// Does this necessary
return Promise.reject(err)
}
)
await axiosValidateUserLoggedIn.get('userloggedin/')
.then((resp) => {
loggedIn.status = resp.data.loggedIn
})
console.log(`inside validateUserLoggedIn - ${loggedIn.status}`)
return loggedIn.status;
}
I have used dj-rest-auth for token authentication.
I built a simple reddit app with React-Redux and when I go to a post page like /posts/mlxft5 and refresh the page it says that the post is not found. I can't figure out how to fix it. Here's my code on codesandbox.
here's my app.js code
function App() {
return (
<Router >
<div className="App">
<div className="App-navbar">
<SubredditsList />
</div>
<Switch>
<Route exact path="/" component={StartMessage}/>
<Route exact path="/search" component={SearchPostsList}/>
<Route exact path="/posts" component={PostsList} />
<Route path="/posts/:postId" component={PostDetailRender}/>
<Route path="/search/:postId" component={SingleSearchPostRender}/>
<Redirect to="/" />
</Switch>
</div>
</Router>
)
}
export default App`
Right now you are only requesting posts from the API when we are on the /posts page for a subreddit. There are no requests initiated by /posts/mlxft. We need to add an additional thunk action that can fetch and store a single post from the id.
We don't want to fetch posts that are already in Redux when we click on a single post from the list on the /posts page. We we will use the condition setting of the createAsyncThunk function to conditionally cancel the fetch if the data already exists.
export const fetchSinglePost = createAsyncThunk(
"posts/fetchSingle",
async (postId) => {
const response = await fetch(
`https://api.reddit.com/api/info/?id=t3_${postId}`
);
const json = await response.json();
return json.data.children[0].data;
},
{
condition: (postId, { getState }) => {
const { posts } = getState();
if (posts.entities[postId]) {
return false;
}
}
}
);
You need to add additional cases in your reducer to handle this thunk. Note: if you use builder callback notation instead of reducer map object notation then you could combine your two "rejected" cases.
[fetchSinglePost.pending]: (state, action) => {
state.status = "loading";
},
[fetchSinglePost.fulfilled]: (state, action) => {
state.status = "succeeded";
postsAdapter.upsertOne(state, action.payload);
},
[fetchSinglePost.rejected]: (state, action) => {
state.status = "failed";
state.error = action.error.message;
}
Inside of your PostDetailRender component you need to dispatch the fetchSinglePost action. It's ok to dispatch it all cases because the thunk itself will cancel the fetching.
useEffect(() => {
dispatch(fetchSinglePost(postId));
}, [dispatch, postId]);
You could potentially have a status for each post rather than one for the whole slice. I explain how to do that in this answer.
Updated CodeSanbox
I also made some changes so that you don't fetch the same subreddit's posts more than once.
I am following these 2 samples:
Webchat with react
Programmatic post activity
My bot is working ok. I can send and process activities via directline. My test helpButton logs ok, but there is no actual 'help' message sent when I click the button like in the sample.
var mainBotConnection;
const { createStore, ReactWebChat } = window.WebChat;
const { createProvider } = window.ReactRedux;
const Provider = createProvider('webchat');
const Store = createStore();
// get a token
const RequestToken = async (user) => {
...
};
(async function () {
RequestToken(agent)
.then(token => {
//init main chat bot
mainBotConnection = window.WebChat.createDirectLine({token: token});
...
//grab mainbot placeholder and put it on screen
window.ReactDOM.render(
<Provider store={Store}>
<ReactWebChat
directLine={mainBotConnection}
storeKey='webchat'
userID={user.id}
username={user.name}
styleOptions={mainBotStyleOptions}
/>
</Provider>,
document.getElementById('webchat'));
// this message does not appear
Store.dispatch({
type: 'WEB_CHAT/SEND_MESSAGE',
payload: { text: 'StartUp hello!' }
});
});
// test button
document.querySelector('#helpButton').addEventListener('click', () => {
// this is successfully logged
console.log(`help button clicked`);
// 'help' text does not appear in bot
Store.dispatch({
type: 'WEB_CHAT/SEND_MESSAGE',
payload: { text: 'help' }
});
// this is also successfully logged
console.log(Store);
});
document.querySelector('#webchat').focus();
})().catch(err => console.error(err));
You need to add store={Store} to your ReactWebChat component:
[...]
<Provider store={Store}>
<ReactWebChat
directLine={mainBotConnection}
storeKey='webchat'
userID={user.id}
username={user.name}
styleOptions={mainBotStyleOptions}
store={Store} // ADD THIS PART
/>
</Provider>,
[...]
That being said, without the rest of your code, I wasn't able to test this exactly. Instead, I started up the React with Redux Sample. If I removed store={Store}, it didn't work, but if I left it in there, it worked just fine and sent both the welcome and help messages. You may also need: <Provider store={ store } key='webchat'>, but like I said, I wasn't able to test your exact code.
I'm trying to turn a block of hardcoded <Route />s into something that's dynamically generated from a config variable. Eg.
From
<Router>
<Switch>
<Route path="/" component={Home} exact />
<Route path="/about" component={About} />
<Route path="/documents" component={Documents} exact />
<Route path="/documents/faq" component={DocFAQ} />
<Route path="/documents/translations" component={Translations} />
</Switch>
</Router>
To
const routes = [
{
path: '/',
component: Home,
exact: true
},
{
path: '/about',
component: About
},
{
path: '/documents',
component: Documents,
children: [
{
path: '/faq',
component: DocFAQ
},
{
path: '/translations',
component: Translations
}
]
}
];
const RecursiveRoute = ({ route, parentPath = '' }) => {
const path = parentPath + route.path;
return (
<React.Fragment>
<Route path={path} exact={route.exact || (route.children != null)} component={route.component} />
{route.children && route.children.map((childRoute, i) => <RecursiveRoute key={i} route={childRoute} parentPath={path} />)}
</React.Fragment>
);
};
const Routes = () => (
<Router>
<Switch>
{routes.map((route, i) => <RecursiveRoute key={i} route={route} />)}
</Switch>
</Router>
);
This code generates exactly what I want when I take the mapping call outside of <Router>; eg. I can verify that it outputs the exact same code as the hardcoded block from before. When it's inside the <Switch> however, only the first route in the routes array is getting mapped--nothing else is generated. Putting logging statements inside <RecursiveRoute> confirms this.
Why is this and how do I fix it?
Another weird thing is that if I paste <RecursiveRoute>'s JSX right into the map statement, it works (except I can't make it recursive in this case):
<Switch>
{routes.map((route, i) => <Route key={i} path={route.path} exact={route.exact || (route.children != null)} component={route.component} />)}
</Switch>
But if I keep it outsourced to another component, the mapping fails again.
[Edit] Solution:
Based on mehamasum's answer, changing <RecursiveRoute> from a component to a function solved this problem:
function generateRecursiveRoute(route, parentPath = '') {
const path = parentPath + route.path;
const routeHasChildren = (route.children != null);
const baseHtml = <Route path={path} exact={route.exact || routeHasChildren} component={route.component} key={path} />;
return !routeHasChildren ?
baseHtml :
<React.Fragment key={path}>
{baseHtml}
{route.children.map((childRoute) => generateRecursiveRoute(childRoute, path))}
</React.Fragment>;
}
const Routes = () => (
<Router>
<Switch>
{routes.map((route) => generateRecursiveRoute(route))}
</Switch>
</Router>
);
Switch is implemented in such a way that you provide multiple Components as children and it will render the first Component's component with matching path. So if you provide something other than a Route as child (something that doesn't have those path and component props) you need to make sure you write your rendering logic in there. Because Switch doesn't know what to do here, as it sees no component or render prop associated with that child.
So when you were mapping to Routes directly they were working, but when you were mapping to RecursiveRoute it was failing. It had no path nor component on it, right?
I have updated your code a little bit so that the routes are directly rendered as children of Switch:
// refactor of RecursiveRoute to
// use it as an utility function, not as component
const getRecursiveRoute = route => {
if (route.children) {
const RootComponent = route.component;
return (
<Route
key={route.path}
path={route.path}
render={props => {
return (
<React.Fragment>
{/* render the root matching component*/}
<RootComponent {...props} />
{/* render the subroutes */}
{/* TODO: consider having a Switch here too */}
{route.children.map(route =>
getRecursiveRoute({
...route,
path: props.match.url + route.path
})
)}
</React.Fragment>
);
}}
/>
);
}
return (
<Route
key={route.path}
path={route.path}
exact={route.exact || route.children != null}
component={route.component}
/>
);
};
const Routes = () => {
return (
<Router>
<Switch>{routes.map(route => getRecursiveRoute(route))}</Switch>
</Router>
);
};
You might have to fix some edge cases about exact routing. Hope it helps.
Recently tried adding redux to one of the apps. One of the solutions is to have root component wrapped within Provider. Did this, but still seeing below issue (in the browser). [Pasted only potentially relevant code from files].
Uncaught Error: Could not find "store" in either the context or props
of "Connect(Gallery)". Either wrap the root component in a ,
or explicitly pass "store" as a prop to "Connect(Gallery)".
client/main.js
document.addEventListener('DOMContentLoaded', function() {
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('mount')
);
});
shared/App./js
class App extends React.Component {
render() {
return (
<BrowserRouter history={ browserHistory }>
<div>
<Route exact path="/" component={Gallery} />
<Route path="/viewitem/:id" component={ViewItem} />
</div>
</BrowserRouter>
);
}
}
shared/redux/index.js
export const reducers = combineReducers({
images: imageReducer,
});
export function configureStore(initialState = {}) {
const store = createStore(
reducers,
initialState,
applyMiddleware(...middleWare)
)
return store;
};
export const store = configureStore();
Could it be that BrowserRouter might not work with redux?