Apollo with service worker in a Next.js project - service-worker

I have a NextJS prototype live at https://www.schandillia.com/blog. The data displayed is being pulled off a Strapi installation at https://dev.schandillia.com/graphql. I also have the entire codebase up on Github at https://github.com/amitschandillia/proost/web (the frontend).
I'm using an Apollo client to interface with the graphql source. And also a service worker set up to enable PWA.
Everything's working fine except I'm unable to cache the query results at the browser. The service worker is able to cache everything else but the results of Apollo queries. Is there any way this could be enabled? The objective is:
To be able to use some kind of prefetching of query results at the server.
To be able to have the results cached at the browser via service worker.
The three files relevant to this issues are as follows:
Apollo Setup
// web/apollo/index.js
import { HttpLink } from 'apollo-link-http';
import { withData } from 'next-apollo';
import { InMemoryCache } from 'apollo-cache-inmemory';
// Set up cache.
const cache = new InMemoryCache();
// Configure Apollo.
const config = {
link: new HttpLink({
uri: 'https://dev.schandillia.com/graphql', // Server URL (must be absolute)
}),
cache,
};
export default withData(config);
Query Component
// web/pages/PostsList.jsx
import ReactMarkdown from 'react-markdown';
import gql from 'graphql-tag';
import { graphql } from 'react-apollo';
import { Fragment } from 'react';
import Typography from '#material-ui/core/Typography';
import CircularProgress from '#material-ui/core/CircularProgress';
const renderers = {
paragraph: props => <Typography variant="body1" gutterBottom {...props} />
};
const PostsList = ({ data: { error, posts } }) => {
let res = '';
if (error) res = (
<Typography variant="subtitle2" gutterBottom>
Error retrieving posts!
</Typography>
);
if (posts && posts.length) {
if (posts.length !== 0) {
// Payload returned
res = (
<Fragment>
{posts.map(post => (
<div>
<Typography variant="display1" gutterBottom>{post.title}</Typography>
<Typography variant="subtitle1" gutterBottom>{post.secondaryTitle}</Typography>
<Typography variant="subtitle2" gutterBottom>Post #{post._id}</Typography>
<ReactMarkdown source={post.body} renderers={renderers} />
</div>
))}
</Fragment>
);
} else {
res = (
// No payload returned
<Typography variant="subtitle2" gutterBottom>
No posts Found
</Typography>
);
}
} else {
res = (
// Retrieving payload
<CircularProgress />
);
}
return res;
};
const query = gql`
{
posts {
_id
title
secondaryTitle
body
}
}
`;
// The 'graphql' wrapper executes a GraphQL query and makes the results
// available on the 'data' prop of the wrapped component (PostsList)
export default graphql(query, {
props: ({ data }) => ({
data,
}),
})(PostsList);
Blog Page
// web/pages/blog.jsx
import React, { PureComponent, Fragment } from 'react';
import PropTypes from 'prop-types';
import Button from '#material-ui/core/Button';
import Typography from '#material-ui/core/Typography';
import { withStyles } from '#material-ui/core/styles';
import Head from 'next/head';
import Link from 'next/link';
import withRoot from '../lib/withRoot';
import PostsList from '../components/PostsList';
const styles = theme => ({
root: {
textAlign: 'center',
paddingTop: theme.spacing.unit * 20,
},
paragraph: {
fontFamily: 'Raleway',
},
});
class Blog extends PureComponent {
constructor(props) {
super(props);
}
componentDidMount() {
if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/serviceWorker.js'); }
}
render() {
const { classes } = this.props;
const title = 'Blog | Project Proost';
const description = 'This is the blog page';
return (
<Fragment>
<Head>
<title>{ title }</title>
<meta name="description" content={description} key="description" />
</Head>
<div className={classes.root}>
<Typography variant="display1" gutterBottom>
Material-UI
</Typography>
<Typography gutterBottom>
<Link href="/about">
<a>Go to the about page</a>
</Link>
</Typography>
<Typography gutterBottom>
<Link href="/blog">
<a>View posts</a>
</Link>
</Typography>
<Button variant="raised" color="primary">
Super Secret Password
</Button>
<Button variant="raised" color="secondary">
Super Secret Password
</Button>
</div>
<PostsList />
</Fragment>
);
}
}
Blog.propTypes = {
classes: PropTypes.shape({
root: PropTypes.string,
}).isRequired,
};
// Posts.propTypes = {
// classes: PropTypes.object.isRequired,
// };
export default withRoot(withStyles(styles)(Blog));
The service worker in question is as follows (redacted for brevity):
// web/offline/serviceWorker.js
const CACHE_NAME = '1b23369032b1541e45cb8e3d94206923';
const URLS_TO_CACHE = [
'/',
'/about',
'/blog',
'/index',
'apple-touch-icon.png',
'browserconfig.xml',
'favicon-16x16.png',
'favicon-194x194.png',
'favicon-32x32.png',
'favicon.ico',
'manifest.json',
];
// Call install event
self.addEventListener('install', (e) => {
e.waitUntil(
caches
.open(CACHE_NAME)
.then(cache => cache.addAll(URLS_TO_CACHE))
.then(() => self.skipWaiting())
);
});
// Call activate event
self.addEventListener('activate', (e) => {
// remove unwanted caches
e.waitUntil(
caches.keys().then((cacheNames) => {
Promise.all(
cacheNames.map((cache) => {
if (cache !== CACHE_NAME) {
return caches.delete(cache);
}
})
);
})
);
});
// Call fetch event
self.addEventListener('fetch', (e) => {
e.respondWith(
fetch(e.request).catch(() => caches.match(e.request))
);
});
Please advise!

Related

How to reload image url one more time if url shows error in loading

I am trying to load images from URL on flatlist using Image Component. In this component there is a property (onError?: () => void ) this property is called on an image fetching error.
When I run my app in low network some images failed to load, so what code should I write in (onError?: () => void ) so that the URL that failed to load images should load one more time in low network.
I am creating this App in React Native for iOS
I have done this :
App.js
import React, { useState } from 'react';
import Product from './product';
import {
FlatList,
SafeAreaView
} from 'react-native';
const products = [
{productImage: "https://media.istockphoto.com/photos/poverty-concept-used-red-shoes-for-children-in-a-thrift-shop-between-picture-id1303292803?s=612x612"},
{productImage: 'https://media.istockphoto.com/photos/poverty-concept-used-red-shoes-for-children-in-a-thrift-shop-between-picture-id1303292803?s=612x612'},
{productImage: "https://media.istockphoto.com/photos/poverty-concept-used-red-shoes-for-children-in-a-thrift-shop-between-picture-id1303292803?s=612x612"},
{productImage: 'https://media.istockphoto.com/photos/poverty-concept-used-red-shoes-for-children-in-a-thrift-shop-between-picture-id1303292803?s=612x612'},
]
const App = () => {
return (
<SafeAreaView>
<FlatList
numColumns={2}
data={products}
keyExtractor={(item, index) => index.toString()}
renderItem={({ item }) => (<Product product={item} />)}>
</FlatList>
</SafeAreaView>
);
};
export default App;
product.js
import React from 'react';
import { View, Image } from 'react-native';
class Product extends React.Component {
constructor(props){
super(props);
this.state = {
uri : this.props.product.productImage,
errorCount : 0
}
}
passInError(e) {
const { productImage } = this.props.product
if (this.state.errorCount < 3) {
this.setState({uri: productImage, errorCount: ++this.state.errorCount})
console.log(" Corrupt Image URL : " + productImage )
console.log(" Corrupt Image Error Reason : ", JSON.stringify(e) )
console.log (" Corrupt Image Reload Count : ", this.state.errorCount)
}
}
render() {
return (
<View>
<Image
style={{ width: 200, height: 200, borderWidth: 2, }}
source={{ uri:this.state.uri }}
onError = {e => this.passInError(e.nativeEvent) }
key = {this.state.errorCount}
/>
</View>
)
}
}
export default Product;
What code should I write in (onError?: () => void ) function to reload failed images URL ?
Try setting image url in state and update when error on loading image.
product.js
import React from 'react';
import { View } from 'react-native';
import FastImage from 'react-native-fast-image';
class Product extends React.Component {
constructor(props){
super(props);
this.state = {
uri : this.props.product.productImage,
errorCount : 0
}
}
render() {
const { productImage } = this.props.product
return (
<View>
<FastImage
style={{ width: 200, height: 200, borderWidth: 2, }}
source={{ uri:this.state.uri }}
resizeMode={FastImage.resizeMode.contain}
onError={e =>
this.state.errorCount < 3 &&
this.setState(
{uri: '', errorCount: ++this.state.errorCount},
() => this.setState({uri: productImage}),
)
}
/>
</View>
)
}
}
export default Product;
If I understand you correctly, you want to try to load the same image for a 2nd time when there is an error on the 1st try. I would try to re-render the component on Error (even better, wrap the image component with a wrapper component so that the whole Product component is not re-rendered):
const Product = () => {
const [fetchCounter, setFetchCounter] = useState(0);
return (
<img
src={'imageUrl'}
onError={() => {
if (fetchCounter < 1) {
setFetchCounter(fetchCounter++);
}
}}
/>
)
}
I do not know your use case, but you can load a fallback image instead. It could look something like this:
const Product = () => {
const [imageUrl, setImageUrl] = useState('product-img-url');
return (
<img
src={imageUrl}
onError={() => setImageUrl('fallback-img-url')}
/>
)
}

Problem with first try simple Relay/React example

I read relay official docs and followed example.
Now, I slightly change it for practice. I replaced load query with useQueryLoader
but result is, Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
I think it is related to loadRepoQuery() part, but without it, TypeError: null is not an object (evaluating 'preloadedQuery.fetchKey') occurs.
I think it's due to my misunderstanding. Please help me.
// App.js
import './App.css';
import graphql from 'babel-plugin-relay/macro';
import {
usePreloadedQuery,
useQueryLoader,
} from 'react-relay/hooks';
// Define a query
const RepositoryNameQuery = graphql`
query AppRepositoryNameQuery {
repository(owner: "facebook", name: "react") {
name
}
}
`;
function RepoRenderer() {
const [repoQueryRef, loadRepoQuery] = useQueryLoader(RepositoryNameQuery);
loadRepoQuery();
return (
<Repo preloadedQuery={repoQueryRef}/>
)
}
function Repo(props) {
const data = usePreloadedQuery(RepositoryNameQuery, props.preloadedQuery);
return (
<div className="App">
<header className="App-header">
<p>{data.repository.name}</p>
</header>
</div>
);
}
export default function App() {
return (
<RepoRenderer/>
);
}
//
import React, { Suspense } from 'react';
import ReactDOM from 'react-dom';
import { RelayEnvironmentProvider } from 'react-relay';
import relayEnvironment from './relayEnvironment';
import App from './App';
import './index.css';
ReactDOM.render(
<React.StrictMode>
<RelayEnvironmentProvider environment={relayEnvironment}>
<Suspense fallback={'Loading...'}>
<App/>
</Suspense>
</RelayEnvironmentProvider>
</React.StrictMode>,
document.getElementById('root')
);
I kind of solve this problem. Here's code snippet for newbie like me.
See this too.
// index.js
ReactDOM.render(
<RelayEnvironmentProvider environment={relayEnvironment}>
<Suspense fallback={'Loading...'}>
<App />
</Suspense>
</RelayEnvironmentProvider>,
document.getElementById('root')
);
//app.js
import React ,{ useCallback } from 'react';
import './App.css';
import graphql from 'babel-plugin-relay/macro';
import {
usePreloadedQuery,
useQueryLoader,
} from 'react-relay/hooks';
// Define a query
const RepositoryNameQuery = graphql`
query AppRepositoryNameQuery {
repository(owner: "yujong-lee", name: "taggy") {
name
}
}
`;
function Repo({queryRef, refetch}) {
const data = usePreloadedQuery(RepositoryNameQuery, queryRef);
return (
<div className="App">
<header className="App-header">
<p>{data.repository.name}</p>
</header>
</div>
);
}
function App() {
const [repoQueryRef, loadRepoQuery] = useQueryLoader(RepositoryNameQuery);
const refetch = useCallback(() => {
loadRepoQuery();
}, [loadRepoQuery])
if(repoQueryRef !== null) {
return <Repo queryRef={repoQueryRef} refetch={refetch}/>
}
return <button type='button' onClick={() => refetch()}>Fetch</button>
}
export default App;

Upload image using react and rails

I am trying to upload image using react into rails active storage.
My component is:
import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import User from './../../Assets/user.png';
import { addAvatar } from './../../actions/userAction';
class UploadAvatar extends Component {
state = {
image: null,
};
fileSelectHandler = (e) => {
this.setState({
image: e.target.files[0],
});
};
fileUploadHandler = () => {
if (this.state.image) {
console.log(this.state.image, this.props.userId);
const fd = new FormData();
fd.append('avatar', this.state.image, this.state.image.name);
this.props.addAvatar({ avatar: fd, userId: this.props.userId });
}
};
render() {
return (
<div className="avatar ">
<div className="avatar-content shadow-lg">
<div className="avatar-pic">
<img src={User} alt="userpic" />
</div>
<p>ADD PHOTO</p>
<input type="file" onChange={this.fileSelectHandler} />
<div className="avatar-foot">
<button type="button" className="skip">
SKIP
</button>
<button type="button" onClick={this.fileUploadHandler} className="submit">
SUBMIT
</button>
</div>
</div>
</div>
);
}
}
const mapStateToProps = store => ({
userId: store.userReducer.userId,
userEmail: store.userReducer.userEmail,
});
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
addAvatar,
},
dispatch,
);
export default connect(
mapStateToProps,
mapDispatchToProps,
)(UploadAvatar);
My ajax.js:
/* eslint-disable no-console no-param-reassign */
let CLIENT_URL = 'http://localhost:3000/api/v1';
function getDefaultOptions() {
return {
method: 'GET',
// credentials: "include",
headers: {
'Content-type': 'application/json; charset=UTF-8',
},
};
}
function buildParam(params, asJSON = true) {
if (asJSON) {
return JSON.stringify(params);
}
const fD = new FormData();
Object.keys(params).forEach((param) => {
fD.append(param, params[param]);
});
return fD;
}
function ajax(uri, options = {}) {
const defaultOptions = getDefaultOptions();
options.method = options.method ? options.method : defaultOptions.method;
if (!options.formType) {
options.headers = options.headers ? options.headers : defaultOptions.headers;
}
options.credentials = options.credentials ? options.credentials : defaultOptions.credentials;
if (options.body && !options.formType) {
options.body = buildParam(options.body);
}
uri = uri.startsWith('/') ? uri : `/${uri}`;
return fetch(`${CLIENT_URL}${uri}`, options)
.then(data => data.json())
.catch(errors => console.log(errors));
}
export default ajax;
But on rails side I am getting empty object in avatar. Don't know why?
Please have a look to screenshot for rails side.
But from postman if I am trying to upload it is working fine.
Is there any alternative way to upload image.

Can this onPress, called in TochableOpacity, return a Text component?

[
the image is the resultof when you console.log(this.state.fixtures)
I havent run into an error, but also havent gotten a result. Just trying to pass the individual match, from the .map(), to the Card component. Not sure if the onPress should be called in the TouchableOpacity. Been looking at this so a couple of day, any feedback s appreciated. Thank You.
import React, { Component } from 'react';
import { View, Text, TouchableOpacity, LayoutAnimation } from 'react-native';
import { Card, CardSection } from '../common';
//import ListOf from './ListOf';
export default class LeagueCard extends Component {
state ={
fixtures: null
}
componentDidMount = () => {
const {league_name, league_id } = this.props.league
{this.getMatches(league_id)}
}
getMatches = (league_id) => {
let legaueID = league_id
let fixArray = []
//console.log(legaueID)
fetch(`https://apifootball.com/api/?action=get_events&from=2016-10-30&to=2016-11-01&league_id=${legaueID}&APIkey=42f53c25607596901bc6726d6d83c3ebf7376068ff89181d25a1bba477149480`)
.then(res => res.json())
.then(fixtures => {
fixtures.map(function(fix, id){
fixArray =[...fixArray, fix]
})
this.setState({
fixtures: fixArray
})
console.log(this.state.fixtures)
})
}
display = () => {
//console.log(this.state.fixtures)
if(this.state.fixtures != null){
this.state.fixtures.map(function(match, id){
//console.log(match)
return (
<Text>match</Text>
)
})
}
}
render(){
const {league_name, league_id } = this.props.league
return(
<View>
<TouchableOpacity
onPress={() => this.display()}
>
<Card>
<CardSection>
<Text>{league_name}</Text>
</CardSection>
</Card>
</TouchableOpacity>
</View>
)}
}
enter code here
import React, {Component} from 'react';
import {View, Text, TouchableOpacity, LayoutAnimation} from 'react-native';
import {Card, CardSection} from '../common';
export default class LeagueCard extends Component {
constructor(props) {
super(props);
this.state = {
fixtures: null,
matches: []
}
}
componentDidMount() {
const {league_name, league_id} = this.props.league;
this.getMatches(league_id)
};
getMatches = (league_id) => {
let legaueID = league_id;
let fixArray = [];
fetch(`https://apifootball.com/api/?action=get_events&from=2016-10-30&to=2016-11-01&league_id=${legaueID}&APIkey=42f53c25607596901bc6726d6d83c3ebf7376068ff89181d25a1bba477149480`)
.then(res => res.json())
.then(fixtures => {
fixtures.map(function (fix, id) {
fixArray = [...fixArray, fix]
});
this.setState({
fixtures: fixArray
})
})
};
display = () => {
if (this.state.fixtures != null) {
this.setState({
matches: this.state.fixtures.map(match => <Text>{match.country_name}</Text>)
});
}
};
render() {
const {league_name, league_id} = this.props.league;
return (
<View>
<TouchableOpacity onPress={this.display}>
<Card>
<CardSection>
{this.state.matches}
</CardSection>
</Card>
</TouchableOpacity>
</View>
)
}
}
I made the changes I think will render your matches for you. I made your display() f set the state.matches of your LeagueCard, which will now be an array of Text components each displaying match. The Array.prototype.map function in JavaScript returns a new array which should then be used.
Also should mention that I added a constructor where I initialize state, though that is not strictly necessary it is a good practice.
Watch out for typos too, you have one in getMatches which I did not fix.
Edit: I changed match to match.country_name as you can't give objects directly to a Text component. You will need to grab each key/value pair of the object you want to display.
this will not show any error to you, but your components will not render, because react will not know where to render it, a workaround to this problem is try something like this
import React, { Component } from 'react';
import { View, Text, TouchableOpacity, LayoutAnimation } from 'react-native';
import { Card, CardSection } from '../common';
//import ListOf from './ListOf';
export default class LeagueCard extends Component {
state ={
fixtures: null,
showTextList: false
}
componentDidMount = () => {
const {league_name, league_id } = this.props.league
{this.getMatches(league_id)}
}
getMatches = (league_id) => {
let legaueID = league_id
let fixArray = []
//console.log(legaueID)
fetch(`https://apifootball.com/api/?action=get_events&from=2016-10-30&to=2016-11-01&league_id=${legaueID}&APIkey=42f53c25607596901bc6726d6d83c3ebf7376068ff89181d25a1bba477149480`)
.then(res => res.json())
.then(fixtures => {
fixtures.map(function(fix, id){
fixArray =[...fixArray, fix]
})
this.setState({
fixtures: fixArray
})
})
}
display = () => {
//console.log(this.state.fixtures)
this.setState({showTextList: true})
}
render(){
const {league_name, league_id } = this.props.league
return(
<View>
<TouchableOpacity
onPress={() => this.display()}
>
<Card>
<CardSection>
<Text>{league_name}</Text>
{this.state.showTextList && this.state.fixtures &&
this.state.fixtures.map((match, id) => (<Text>{match}</Text>))
}
</CardSection>
</Card>
</TouchableOpacity>
</View>
)}
}
I just put the text list inside your CardSection, because i belive you want to render the list inside it,but feel free to put this wherewver you want

React Native, Webview iOS not rendering

I'm building an app with React-Native.
I'm trying to render a URL with a Webview, even some basic HTML or a site like m.facebook.com will not render in iOS. Tried different solution like other Webviews but none give results.
When in Android i don't have any problems and the page(s) will render just fine. Am i missing some key information.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { actions } from 'react-native-navigation-redux-helpers';
import { Container, Header, Title, Content, Button, Icon, Footer } from 'native-base';
import { Platform, WebView } from 'react-native';
import FooterTabs from '../../components/footerTabs/FooterTabs';
import { openDrawer } from '../../actions/drawer';
import { setWebsiteUrl } from '../../actions/website';
import styles from './styles';
const {
pushRoute,
popRoute,
} = actions;
class Website extends Component {
constructor(props, context) {
super(props, context);
this.state = {};
this.openFrame = this.openFrame.bind(this);
}
popRoute() {
this.props.popRoute(this.props.navigation.key);
}
pushRoute(route, index) {
this.props.pushRoute({ key: route, index: 1 }, this.props.navigation.key);
}
openFrame(url, name) {
this.props.setWebsiteUrl(url, name);
this.pushRoute('website', 2);
}
render() {
const { props } = this;
const { website, totalQuantity } = props;
const { frameUrl, frameName } = website;
return (
<Container style={styles.container} theme={deenTheme}>
<Header toolbarDefaultBg="#FFF" toolbarTextColor="FBFAFA">
<Button transparent onPress={this.props.openDrawer}>
<Icon name="ios-menu" />
</Button>
<Title style={styles.headerText}>{frameName}</Title>
<Button transparent onPress={() => this.openFrame('/cart', 'Winkelwagen')}>
<Icon style={{ fontSize: 25 }} name="md-basket" />
</Button>
<Button transparent>
<Icon style={{ fontSize: 25 }} name="md-search" />
</Button>
</Header>
<Content>
<WebView
source={{ uri: frameUrl }}
startInLoadingState
javaScriptEnabledAndroid
javaScriptEnabled
domStorageEnabled
scrollEnabled
style={{ flex: 1, width: 320 }}
/>
</Content>
<Footer theme={deenTheme}>
<FooterTabs />
</Footer>
</Container>
);
}
}
Website.propTypes = {
totalQuantity: React.PropTypes.number,
openDrawer: React.PropTypes.func,
setWebsiteUrl: React.PropTypes.func,
popRoute: React.PropTypes.func,
pushRoute: React.PropTypes.func,
navigation: React.PropTypes.shape({
key: React.PropTypes.string,
}),
};
function bindAction(dispatch) {
return {
openDrawer: () => dispatch(openDrawer()),
popRoute: key => dispatch(popRoute(key)),
pushRoute: (route, key) => dispatch(pushRoute(route, key)),
setWebsiteUrl: (url, name) => dispatch(setWebsiteUrl(url, name)),
};
}
const mapStateToProps = state => ({
navigation: state.cardNavigation,
website: state.website,
totalQuantity: state.cart.totalQuantity,
});
export default connect(mapStateToProps, bindAction)(Website);
UPDATE!
in iOS i need to configure style width & height else it won't work.

Resources