Working on persisting login state, Is there any better way or more clean approach? - react-hooks

I am working on Authentication part of my hobby project ,I am using redux,jwt,Django,Nextjs.
what I am doing -> first user will login, on successful login response from login API get stored in localStorage of browser and after some time when user again visit my website accessToken expire time is compared to current time and user is logged in if access Token expire time is greater then current time
**This Approach is working fine but I am looking for more better ways to do this and when i console.log any state value it logs more then 1 time and sometimes more then 2-3 times **
A Custom hook for handle Autologin when user reopen the home page after login in past
import React, { useEffect, useMemo, useState } from "react";
import jwt_decode from "jwt-decode";
const checkAuthHook = () => {
const [cookie, setCookie] = useState([]);
const [tokenTimeStamp, SetTokenTimeStamp] = useState(0);
const [ExpireTime, setExpireTime] = useState();
const [dataStatus, setDataStatus] = useState(false);
const [flag, setFlag] = useState(false);
const [secondFlag, setSecondFlag] = useState(false);
const [relogin, setRelogin] = useState(false);
const [logoutStatus, setlogoutStatus] = useState(false);
const [email, setEmail] = useState("");
const [token, setToken] = useState("");
useEffect(() => {
const data = localStorage.getItem("user");
if (data) {
var localToken = JSON.parse(data);
setCookie(localToken);
setDataStatus(!dataStatus);
}
}, []);
useMemo(() => {
if (cookie.length > 0) {
setEmail(cookie[1]);
setToken(cookie[0]);
var decodeToken = jwt_decode(cookie[0]);
SetTokenTimeStamp(decodeToken.exp);
setSecondFlag(!secondFlag);
}
}, [dataStatus]);
useMemo(() => {
if (tokenTimeStamp > 0) {
var localex = new Date(tokenTimeStamp * 1000);
setExpireTime(localex);
setFlag(!flag);
}
}, [secondFlag]);
useMemo(() => {
var current = new Date();
const Expire = ExpireTime;
if (current > Expire || current == Expire) {
localStorage.removeItem("user");
setlogoutStatus(true);
}
if (current < Expire) {
setRelogin(true);
}
}, [flag]);
return { ExpireTime, logoutStatus, relogin, email, token };
};
export default checkAuthHook;
this is home page
import Head from "next/head";
import { login, logout } from "./features/auth/userSlice";
import Navbar from "./components/Navbar";
import Hero from "./components/Hero";
import { useInitFbSDK } from "./FB_hook";
import { useEffect, useMemo, useState } from "react";
import checkAuthHook from "./helpers/checkAuthHook";
import { useDispatch } from "react-redux";
// import checkAuthHook from "./helpers/checkAuthHook";
export default function Home() {
const dispatch = useDispatch();
const isFbSDKInitialized = useInitFbSDK();
const { ExpireTime, relogin, logoutStatus, email, token } = checkAuthHook();
useMemo(() => {
if (
email != "" &&
ExpireTime !== undefined &&
relogin == true &&
token != ""
) {
console.log(ExpireTime);
dispatch(
login({
email: email,
token: token,
loggedIn: true,
})
);
} else {
dispatch(logout({ email: "", token: "" }));
}
}, [relogin]);
// console.log(logoutStatus);
return (
<>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className=" bg-[url('../public/background.svg')] bg-contain bg-no-repeat h-screen">
<Navbar />
<Hero />
</main>
</>
);
}
Am i use Hooks correctly or not ?

Related

Why are the components not rerendering after Response?

Table with pinia store equipmentlist, fethced from usequipments but table does not rerender
VueTableComponent
<script setup lang="ts">
import { useEquipmentStore } from '~/store/equipmentStore'
import useEquipments from '~/composables/equipments'
const { getEquipmentList } = useEquipments()
onBeforeMount(() => { getEquipmentList() })
const state = useEquipmentStore()
const equipmentList: any = state.equipmentList
const loaded = state.loaded
</script>
<template>
<el-table :key="loaded" :data="equipmentList" style="width: 100%">
<el-table-column type="expand">
<template #default="props">
ID: {{ props.row.id }}
</template>
</el-table-column>
<el-table-column label="ID" prop="id" />
<el-table-column label="Name" prop="name" />
</el-table>
</template>
Typescript File for all CRUD Operations equipment.ts
import { useRouter } from 'vue-router'
import http from '../http-common'
import { useEquipmentStore } from '~/store/equipmentStore'
export default function useEquipments() {
const state = useEquipmentStore()
const errors = ref([]) // array of strings
const router = useRouter()
const getEquipmentList = async() => {
try {
console.log(state.equipmentList.length)
const response = await http.get('/equipment/list')
state.equipmentList = response.data
console.log(state.equipmentList.length)
console.log(state.equipmentList[0])
}
catch (error: any) {
console.log(error.message)
}
}
Equipment(Pinia)Store
import { defineStore } from 'pinia'
import type { Ref } from 'vue'
export const useEquipmentStore = defineStore('equipment', {
state: () => ({
equipment: ref({}) as any,
equipmentList: ref([]) as Ref<any[]>,
}),
actions: {
reset() {
this.equipment = {}
this.equipmentList = []
},
},
})
1. i called several times getEquipment list and it is faster done then i stored an initial equipment, 2. i clicked on the link on the left and fetched several times more and as u can see there is something fetchd but not displayed, 3. after repeating to home and again to Link the component is there and alll other next fetches do indeed function well
Main.ts
app.component('InputText', InputText)
app.mount('#app')
useEquipments().initDB()
}
same fetching class equipment.ts
const initDB = () => {
try {
if (state.equipmentList.length === 0) { storeEquipment({ id: 1, name: 'Firma' }) }
else {
for (const equipment of state.equipmentList) {
console.log(equipment)
if (equipment === 'Firma')
state.equipmentList.splice(state.equipmentList.indexOf(equipment), 1)
}
}
}
catch (error: any) {
console.log(error.message)
}
}

Can't use React useEffect and also build failed using Gatsby

I am building a headless eCommerce website using Nacelle, Gatsby, and Shopify plus.
My problem is that I integrated Okendo API to fetch product reviews and can't build the project.
Actually, as you know, headless eCommerce is a new technology to us, but it is mostly close to Gatsby and SSR.
I tried to go 2 ways, one is to include the script to head using gatsby-react-helmet, and another one is to call window api inside useEffect or useLayoutEffect.
1. Including the script to head tag using gatsby-plugin-react-helmet.
ProductReview.js
import React, { useEffect } from 'react';
import { Helmet } from 'react-helmet';
import transformProductId from '../../utils/transformProductId';
import { PRODUCT_REVIEW_METAFIELD_KEY, OKENDO_SUBSCRIBER_ID } from '../../constants';
const ProductReview = ({
product
}) => {
const OkendoSettings = {
filtersEnabled: true,
omitMicrodata: true,
subscriberId: OKENDO_SUBSCRIBER_ID,
widgetTemplateId: "default"
}
return (
<>
<Helmet>
<script type="application/javascript" src="../plugins/okendo/index.js" />
<script type="application/json" id="oke-reviews-settings">
{JSON.stringify(OkendoSettings)}
</script>
<script type="application/javascript" src="../plugins/okendo/initAPI.js" />
</Helmet>
<div
data-oke-reviews-widget
data-oke-reviews-product-id={transformProductId(product.id)}
/>
</>
);
};
export default React.memo(ProductReview);
/plugin/okendo/index.js
(function () {
function asyncLoad() {
var urls = ['https:\/\/d3hw6dc1ow8pp2.cloudfront.net\/reviewsWidget.min.js?shop=example.myshopify.com'];
for (var i = 0; i < urls.length; i++) {
var s = document.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.src = urls[i];
var x = document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
}
}
if (window.attachEvent) {
window.attachEvent('onload', asyncLoad);
} else {
window.addEventListener('load', asyncLoad, false);
}
})();
/plugin/okendo/initAPI.js
window.okeReviewsWidgetOnInit = function (okeInitApi) {};
If I include the Okendo scripts to head tag, it works all fine.
But when I try to build on vercel, it says "error Building static HTML failed for path /products/example-product-slug".
2. Calling window.init api inside useEffect.
ProductReview.js
import React, { useEffect } from 'react';
import { Helmet } from 'react-helmet';
import transformProductId from '../../utils/transformProductId';
import { PRODUCT_REVIEW_METAFIELD_KEY, OKENDO_SUBSCRIBER_ID } from '../../constants';
const ProductReview = ({
product
}) => {
const OkendoSettings = {
filtersEnabled: true,
omitMicrodata: true,
subscriberId: OKENDO_SUBSCRIBER_ID,
widgetTemplateId: "default"
}
useEffect(() => {
if (typeof window !== `undefined` && window.okendoInitApi) {
const reviewsWidget = window.document.querySelector('#oke-reviews-widget');
window.okendoInitApi.initReviewsWidget(reviewsWidget);
}
}, [product.id]);
return (
<>
<Helmet>
<script type="application/javascript" src="../plugins/okendo/index.js" />
<script type="application/json" id="oke-reviews-settings">
{JSON.stringify(OkendoSettings)}
</script>
{/* <script type="application/javascript" src="../plugins/okendo/initAPI.js" /> */}
</Helmet>
<div
id="oke-reviews-widget"
data-oke-reviews-widget
data-oke-reviews-product-id={transformProductId(product.id)}
/>
</>
);
};
export default React.memo(ProductReview);
While I am using useEffect to initialize Okendo api, it works only when the page refresh, not work if I open a page.
And if I try to build it, it says "error "window" is not available during server side rendering.".
I know useEffect doesn’t run unless it’s in the browser, but still I don't get what the solution is.
Hope to hear a good news.
Thank you.
UPDATE: The product id is generated from Shopify product graphql data named handle.
gatsby-node.js
exports.createPages = async ({ graphql, actions: { createPage } }) => {
// Fetch all products
const products = await graphql(`
{
allNacelleProduct (filter: { availableForSale: {eq: true} }) {
edges {
node {
handle
}
}
}
}
`);
products.data.allNacelleProduct.edges.forEach((product) =>
createPage({
// Build a Product Detail Page (PDP) for each product
path: `/products/${product.node.handle}`,
component: path.resolve('./src/templates/product-detail.js'),
context: {
handle: product.node.handle
}
})
);
...

[updated]Intergrating NextJS and Redux State Management

this my updated version of intergrating redux and NextJS. Just to elobarate what I have done so far...
STEP 1. I've created a store.js file to set up my global store in reference to github's explanation from nextJS developers.
import { createStore, applyMiddleware, combineReducers } from 'redux';
import { createWrapper, HYDRATE } from 'next-redux-wrapper';
import thunkMiddleware from 'redux-thunk';
import { customerListReducer } from './customerReducers';
const bindMiddleware = (middleware) => {
if (process.env.NODE_ENV !== 'production') {
const { composeWithDevTools } = require('redux-devtools-extension');
return composeWithDevTools(applyMiddleware(...middleware));
}
return applyMiddleware(...middleware);
};
const combinedReducer = combineReducers({
customerList: customerListReducer,
});
const reducer = (state, action) => {
console.log('Just Displaying the Store', state);
if (action.type === HYDRATE) {
const nextState = {
...state, // use previous state
...action.payload, // apply delta from hydration
};
if (state.count) nextState.count = state.count; // preserve count value on client side navigation
return nextState;
} else {
return combinedReducer(state, action);
}
};
// create a makeStore function
const store = () =>
createStore(
reducer,
bindMiddleware([thunkMiddleware])
);
// export an assembled wrapper
export const wrapper = createWrapper(store);
STEP 2: Imported the wrapper above in my _app file to make the wrapper available across all pages in my application
import Nav from '../components/Nav';
import {wrapper} from '../reducers/store';
function MyApp({ Component, pageProps }) {
return (
<>
<Nav />
<Component {...pageProps} />
</>
);
}
export default wrapper.withRedux(MyApp);
STEP 3: CONFIGURATIONS
A) My Action that calls external API
import axios from 'axios';
import {
CUSTOMER_LIST_REQUEST,
CUSTOMER_LIST_SUCCESS,
CUSTOMER_LIST_FAIL,
} from '../constants/customerConstants';
export const listCustomers = () => async (dispatch) => {
try {
dispatch({
type: CUSTOMER_LIST_REQUEST,
});
const { data } = await axios.get(
'https://byronochara.tech/gassystem/api/v1/customers'
);
const result = data.results;
dispatch({
type: CUSTOMER_LIST_SUCCESS,
payload: result,
});
} catch (error) {
dispatch({
type: CUSTOMER_LIST_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
B)My Action Reducer
import {
CUSTOMER_LIST_REQUEST,
CUSTOMER_LIST_SUCCESS,
CUSTOMER_LIST_FAIL,
} from '../constants/customerConstants';
import { HYDRATE } from 'next-redux-wrapper';
export const customerListReducer = (state = { customers: [] }, action) => {
switch (action.type) {
case HYDRATE:
return { loading: true, customers: [] };
case CUSTOMER_LIST_REQUEST:
return { loading: true, customers: [] };
case CUSTOMER_LIST_SUCCESS:
return {
loading: false,
customers: action.payload,
};
case CUSTOMER_LIST_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
C)The finally bringing it all together in my index.js page to display the results:
import React, { useEffect } from 'react';
import Head from 'next/head';
import { useSelector} from 'react-redux';
import { listCustomers } from './../actions/customerActions';
import { wrapper } from '../reducers/store';
import styles from '../styles/Home.module.css';
const Home = () => {
//Select the loaded customers' list from central state
const customerList = useSelector((state) => {
console.log(state);
return state.customerList;
});
const { loading, error, customers } = customerList;
//displaying the customers data from the external API
console.log('Fetched Customers Data', customers);
return (
<div className={styles.container}>
<Head>
<title>Home | Next</title>
</Head>
<h1>Welcome to Home Page</h1>
{/* {loading && <h6>Loading...</h6>} */}
{/* {error && <h6>Error Occured...</h6>} */}
{/* {customers.map((customer) => (
<h3>{customer.customerName}</h3>
))} */}
{/* <ArticleList customers={customers} /> */}
</div>
);
};
// getStaticProp at build time
// getServerSideProp at every request slower
// getStaticPath to dynamically generate paths based on the data we are fetching
export const getStaticProps = wrapper.getServerSideProps(async ({ store }) => {
// console.log('STORE', store);
store.dispatch(listCustomers());
});
export default Home;
COMMENT ON THE PROBLEM I'M FACING FROM THE ABOVE CODE: once everything has been set up if you follow the code above, the code seems to run well the store is successfully created when I log the result on the console ``{ customerList: { loading: true, customers: [] } }. But then I guess this is the result from the HYDRATE action type since it will always be dispatch since am using getStaticProps``` that creates a new store instance in the server.
MAIN QUIZ: My challenge is how do I bypass the HYDRATED action and reconcile the server side state with the client side store and persist it and at least to finally be able to view the list from the external API. Thanks in advance. :)
I totally recommend you to use reduxjs/toolkit. It's very simple , less code, no wrappers, clean. And no matter your project on nextjs or created via CRA. Also you dont need to configure redux-thunk and redux-devtools cause they are enabled by default. Read documentation for more information ( how to persist state without any npm package and so on )
Here is a little example.
store.js
import { combineReducers, configureStore } from "#reduxjs/toolkit";
import userSlice from './user.slice.js';
//reducers
const rootReducer = combineReducers({
user: userSlice
});
const store = configureStore({
reducer: rootReducer,
});
export default store;
Wrap with Provider (in your case _app.js)
<Provider store={store}>
<Component {...pageProps} />
</Provider>
user.slice.js ( action + reducer )
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
const initialState = {
id: '',
email: '',
roles: []
};
// export async action
export const signIn = createAsyncThunk('user/signIn', async (data) => {
try {
const payload = await api.auth.signin(data).then((res) => res.data);
// do some stuff if you want
return payload ;
} catch (err) {
console.log(err.response);
}
});
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
removeUser(state, payload) {
//cant be an async method
return initialState;
},
extraReducers: (builder) => {
builder.addCase(signIn.fulfilled, (state, { payload }) => {
// payload from the async method above (asyncThunk)
return payload;
});
},
},
});
// export actions
export const { removeUser } = userSlice.actions;
// export reducer
export default userSlice.reducer;
Thats it. Last step to call actions from any component e.g.
import { useDispatch, useSelector } from 'react-redux';
import { signIn, removeUser } from '../actions/userSlice';
// in function component
// call hooks
const dispatch = useDispatch();
// read the store
const { user } = useSelector((state) => state);
// dispatch any action , example below
dispatch(signIn(userCredentials));
// or
dispatch(removeUser());
I has an Issue with setting Redux with NextJS and this is my final answer after some insight from mirik999 too.
A. my store.
import { configureStore } from '#reduxjs/toolkit';
//importing the slice file with sliced reducers
import customerListReducer from '../slices/customerSlice';
// const composedEnhancer = composeWithDevTools(applyMiddleware(thunkMiddleware));
const store = configureStore({
reducer: {
customerList: customerListReducer,
},
});
export default store;
B. The store is provided in my app component
function MyApp({ Component, pageProps }) {
return (
<Provider store={store}>
<Nav />
<Component {...pageProps} />
</Provider>
);
}
export default MyApp;
C. The Slice file that automatically creates action creators and the reducer
import { createSlice } from '#reduxjs/toolkit';
//creating and action that calls API from a REST API backend
export const customersFetchedList = createAsyncThunk(
'customersList/customersListSuccess',
async () => {
try {
const { data } = await axios.get(
'https://example.com/api/your/endpoint'
);
const result = data.results;
//the payload
const payload = result;
return payload;
} catch (error) {
console.log(error.response);
const payload =
error.response && error.response.data.message
? error.response.data.message
: error.message;
return payload;
}
}
);
const initialState = {
loading: true,
customers: [],
error: false,
};
const customerListSlice = createSlice({
name: 'customersList',
initialState,
reducers: {
//reducer functions we've provided
customersRequest(state, action) {
if (state.loading == true) {
return state;
}
},
},
extraReducers: (builder) => {
initialState,
builder.addCase(customersFetchedList.fulfilled, (state, action) => {
state.loading = false;
state.customers = action.payload;
state.error = false;
return state;
});
},
});
export const {
customersRequest,
customersLoadingError,
} = customerListSlice.actions;
export default customerListSlice.reducer;
D. Then finally fired this action above in my component using the useEffect()
import React, { useEffect } from 'react';
import Head from 'next/head';
const Home = () => {
//method to fire the action
const dispatch = useDispatch();
//Select the loaded customers' list from central state
const customerList = useSelector((state) => state);
// const { loading, error, customers } = customerList;
useEffect(() => {
dispatch(listCustomers());
}, []);
return (
<div className={styles.container}>
<Head>
<title>Home | Next</title>
</Head>
<h1>Welcome to Home Page</h1>
{loading && <h6>Loading...</h6>}
{error && <h6>Error Occured...</h6>}
{customers.map((customer) => (
<h3>{customer.customerName}</h3>
))}
</div>
);
};
Thanks so much for your contribution. :)

redirect dependent on ajax result using react

I would like to redirect to a component in case the data of the success has a certain value.
When ajax returns the data, depending on the value of the data redirected to the Contents class that I previously imported.
I've been looking for information about the push method
My error is: Error: Invariant failed: You should not use <Redirect> outside a <Router>
import React, { Component } from 'react';
import { Modal,Button } from 'react-bootstrap';
import $ from 'jquery';
import { Redirect } from 'react-router';
import Contents from './Contents';
class Login extends Component {
constructor(props, context) {
super(props, context);
this.handleShow = this.handleShow.bind(this);
this.handleClose = this.handleClose.bind(this);
this.handleloginClick = this.handleloginClick.bind(this);
this.handleUsernameChange = this.handleUsernameChange.bind(this);
this.handlePasswordChange = this.handlePasswordChange.bind(this);
this.state = {
show: true,
username: "",
password: "",
};
}
handleloginClick(event) {
var parametros = {
username: this.state.username,
password: this.state.password
}
const { history } = this.props;
$.ajax({
data: parametros,
url: "https://privada.mgsehijos.es/react/login.php",
type: "POST",
success: function (data) {
}
});
}
handleUsernameChange(event) {
this.setState({username: event.target.value});
}
handlePasswordChange(event) {
this.setState({password: event.target.value});
}
handleClose() {
this.setState({ show: false });
}
handleShow() {
this.setState({ show: true });
}
render() {
If(Condicion){
return (<Redirect to={'./Contents'} />);
}
return (
//Here my modal.
);
}
}
export default Login;
you can use Router dom to navigate.
My fiddle: https://jsfiddle.net/leolima/fLnh9z50/1/
const AboutUs = (props) => {
console.log(props.location.state)
console.log('Hi, you are in About page, redirecting with router dom in 3 seconds')
setTimeout(() => {
props.history.push('/')}, 3000);
return <h1>Now we're here at the about us page.</h1>;
};
Full Example:
// Select the node we wish to mount our React application to
const MOUNT_NODE = document.querySelector('#app');
// Grab components out of the ReactRouterDOM that we will be using
const { BrowserRouter, Route, Switch, NavLink, Link } = window.ReactRouterDOM;
// PropTypes is used for typechecking
const PropTypes = window.PropTypes;
// Home page component
const Home = () => {
return <h1>Here we are at the home page.</h1>;
};
// AboutUs page component
const AboutUs = (props) => {
console.log(props.location.state)
return <h1>Now we're here at the about us page.</h1>;
};
// NotFoundPage component
// props.match.url contains the current url route
const NotFoundPage = ({ match }) => {
const {url} = match;
return (
<div>
<h1>Whoops!</h1>
<p><strong>{url.replace('/','')}</strong> could not be located.</p>
</div>
);
};
// Header component is our page title and navigation menu
const Header = () => {
// This is just needed to set the Home route to active
// in jsFiddle based on the URI location. Ignore.
const checkActive = (match, location) => {
if(!location) return false;
const {pathname} = location;
return pathname.indexOf('/tophergates') !== -1 || pathname.indexOf('/_display/') !== -1;
}
return (
<header>
<h1>Basic React Routing</h1>
<nav>
<ul className='navLinks'>
{/* Your home route path would generally just be '/'' */}
<li><NavLink to="/tophergates" isActive={checkActive}>Home</NavLink></li>
<li><Link to={{
pathname: "/about",
state: { fromDashboard: true }
}}>About</Link></li>
</ul>
</nav>
</header>
);
};
// Out layout component which switches content based on the route
const Layout = ({children}) => {
return (
<div>
<Header />
<main>{children}</main>
</div>
);
};
// Ensure the 'children' prop has a value (required) and the value is an element.
Layout.propTypes = {
children: PropTypes.element.isRequired,
};
// The top level component is where our routing is taking place.
// We tell the Layout component which component to render based on the current route.
const App = () => {
return (
<BrowserRouter>
<Layout>
<Switch>
<Route path='/tophergates' component={Home} />
<Route path='/_display/' component={Home} />
<Route exact path='/' component={Home} />
<Route path='/about' component={AboutUs} />
<Route path='*' component={NotFoundPage} />
</Switch>
</Layout>
</BrowserRouter>
);
};
// Render the application
ReactDOM.render(
<App />,
MOUNT_NODE
);

Redux server side rendering breaks when using an apollo graphql client

When I'm using an apollo provider with redux server side rendering,
https://github.com/reactjs/redux/blob/master/docs/recipes/ServerRendering.md
I get the following warning and it breaks the server side output
Warning: Failed context type: The context `client` is marked as required in `Apollo(Home)`, but its value is `undefined`.
in Apollo(Home) (created by Connect(Apollo(Home)))
in Connect(Apollo(Home)) (created by RouterContext)
in RouterContext
in Provider
However this renders fine client side.
app
window.webappStart = () => {
const initialState = window.__PRELOADED_STATE__;
const store = createStore(rootReducer, initialState);
const client = new ApolloClient({
networkInterface: createNetworkInterface({ uri: 'https://api.graph.cool/simple/v1/foo' }),
});
render(
<ApolloProvider store={store} client={client}>
<Router>{routes}</Router>
</ApolloProvider>,
document.querySelector(".js-content")
);
};
Here's the boilerplate apollo code
import React from 'react';
import gql from 'graphql-tag';
import { graphql } from 'react-apollo';
// The data prop, which is provided by the wrapper below contains,
// a `loading` key while the query is in flight and posts when it is ready
function PostList({ data: { loading, posts } }) {
if (loading) {
return <div>Loading</div>;
} else {
return (
<ul>
{posts.map(post =>
<li key={post.id}>
{post.title} by {' '}
{post.author.firstName} {post.author.lastName} {' '}
({post.votes} votes)
</li>
)}
</ul>
);
}
}
// The `graphql` wrapper executes a GraphQL query and makes the results
// available on the `data` prop of the wrapped component (PostList here)
export default graphql(gql`
query allPosts {
posts {
id
title
votes
author {
id
firstName
lastName
}
}
}
`)(PostList);
The PostList component looks alright to me, as does the client-side initiation of your app.
If you're getting that error in your server logs, then I think you'll want to check your routing middleware to ensure you're passing the client to ApolloProvider before rendering your app.
I'm using Express v4.* and react-router v4. My setup looks like this:
import React from 'react'
import { renderToString } from 'react-dom/server'
import { match, RouterContext } from 'react-router'
import ApolloClient, { createNetworkInterface } from 'apollo-client'
import { ApolloProvider, renderToStringWithData } from 'react-apollo'
import routes from '../app/routes.js'
import { store } from 'app/store/index.js'
const Html = ({ title = 'App', content }) => (
<html>
<head>
<title>{title}</title>
<link href="/main.css" rel="stylesheet"/>
</head>
<body>
<div id="root" dangerouslySetInnerHTML={{ __html: content }} />
<script src='/index.js'/>
</body>
</html>
)
module.exports = (req, res) => {
match(
{
location: req.originalUrl,
routes,
},
(error, redirectLocation, renderProps) => {
if (redirectLocation) {
res.redirect(redirectLocation.pathname + redirectLocation.search)
} else if (error) {
console.error('ROUTER ERROR:', error)
res.status(500)
} else if (renderProps) {
const client = new ApolloClient({
ssrMode: true,
networkInterface: createNetworkInterface({
uri: 'http://localhost:8888/graphql',
}),
})
/**
* Make sure client is added here. Store is optional */
const App = (
<ApolloProvider client={client} store={store}>
<RouterContext {...renderProps} />
</ApolloProvider>
)
/**
* Render and send response
*/
renderToStringWithData(App).then(content => {
const html = <Html content={content}/>
res.status(200).send(`<!DOCTYPE html>\n${ renderToString(html) }`)
}).catch((err) => console.log(`INITIAL RENDER (SSR) ERROR:`, err))
} else {
res.status(404).send('Not found')
}
}
)
}

Resources