I have a react component using react-apollo fetching a list. The server is using relay style connection.
My question is when I fetch for next page, it shows the "Loading..." and then the page cursor moves back to the top, shown in the following.
https://imgur.com/a/ImfQPVJ
I want to have a better UX that no "Loading..." text shown, and after fetching, just append the newly fetched result to the back of the list.
This is the code (remove non-relevant code):
class Links extends Component {
fetchNextPage = (pageInfo, fetchMore) => ev => {
const { linksOrder } = this.props;
const after = pageInfo.endCursor;
const queryVars = getQueryVarsFromParam(linksOrder, after);
fetchMore({
variables: queryVars,
updateQuery: (previousResult, { fetchMoreResult }) => {
const { allLinks: { nodes: newLinks, pageInfo: newPageInfo }} = fetchMoreResult;
// Handle no new result
if (newLinks.length === 0) return previousResult;
// With new result, we append to the previous result list
let finalResult = previousResult;
finalResult.allLinks.pageInfo = newPageInfo;
finalResult.allLinks.nodes = finalResult.allLinks.nodes.concat(newLinks);
return finalResult;
}
})
}
render() {
const { linksOrder, classes } = this.props;
const { linkGqlCursorAfter: after } = this.state;
const queryVars = getQueryVarsFromParam(linksOrder, after);
return(<Query query={LINKS_QUERY_GQL} variables={queryVars}>
{({ loading, error, data, fetchMore }) => {
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;
const { allLinks: { nodes: links, pageInfo }} = data;
return(
<React.Fragment>
<Grid container spacing={8} className={ classes.linksList } >
{ links.map((link, ind) => (
<Grid item key={link.id}><Link ind={ind} link={link}/></Grid>
)) }
{ pageInfo.hasNextPage ? (
<Grid item key={ "fetchNextPage" } style={{ alignSelf: "center" }}>
<Fab onClick={ this.fetchNextPage(pageInfo, fetchMore) }
size="small" color="secondary"><AddIcon /></Fab>
</Grid>)
: null }
</Grid>
</React.Fragment>
)
} }
</Query>)
}
}
How could I achieve that? Another approach I could think of is not to use <Query> tag at all, and retrieve the Apollo client itself via HOC in onClick hander, fetch the result, and add the result back to the links object by updating the component state.
Then, this begs the question of why we want to use <Query> <Mutation>, when we can always get the Apollo client and handle the query interaction better ourselves?
Since you already have the data you fetched from previous pages in your cache you can render that conditionally in your if (loading) while waiting for new data to be fetched.
if(loading) {
return data.allLinks ?
<>
<LinksComponent {/*pass in the old data here...*/}/>
<Spinner/>
</> : <Spinner/>
}
If you already have data.allLinks you will display that data in a LinksComponent even when new data is being fetched. The Spinner component will be displayed under the LinksComponent while Loading is true. If you don't have any data fetched, you will just display the Spinner component.
Related
I'm trying to create a search filter that will filter through facility names that lives in an array of objects.If I hard code an array into the state the filter works, but I need it to drab the info from props. The filtered list is being generated and showing all of the names on the screen but when I type it the textbox to filter nothing happens. What have I overlooked?
class FacilitySearch extends React.Component {
constructor(props) {
super(props);
this.state = {
search: ""
};
}
componentDidMount() {
this.props.dispatch(actions.getFacilitiesList());
}
//The subsr limits the # of characters a user can enter into the seach box
updateSearch = event => {
this.setState({ search: event.target.value.substr(0, 10) });
};
render() {
if (!this.props.facilityList) {
return <div>Loading...</div>
}
let filteredList = this.props.facilityList;
filteredList.filter(facility => {
return facility.facilityName.toLowerCase().indexOf(this.state.search.toLowerCase()) !== -1;
});
return (
<div>
<input
type="text"
value={this.state.search}
onChange={this.updateSearch.bind(this)}
placeholder="Enter Text Here..."
/>
<ul>
{filteredList.map(facility => {
return <li key={facility.generalIdPk}>{facility.facilityName}</li>;
})}
</ul>
</div>
);
}
}
const mapStateToProps = state => ({
facilityList: state.facilityList.facilityList
});
export default connect(mapStateToProps)(FacilitySearch)
The problem is that you are not storing the return value of filter in any variable.
You should do something like:
let filteredList = this.props.facilityList.filter(facility => {
return facility.facilityName.toLowerCase().indexOf(this.state.search.toLowerCase()) !== -1;
});
From MDN:
The filter() method creates a new array with all elements that pass the test implemented by the provided function.
I am able to get the Apollo query component in my application. But I'm having a hard time adding the mutation. I have the mutation query written out. I want to use the row and cellvalue from the cellEditProp hook function and pass them into the mutation component. I am having a hard time figuring out where to nest or wrap the mutation component. Any tips are much appreciated.
function onSaveCell(row, cellName, cellValue) {
//Need to use this data for the mutation
}
function onBeforeSaveCell(row, cellName, cellValue) {
console.log(cellName, cellValue, row
);
return true;
}
const cellEditProp = {
mode: 'click',
blurToSave: true,
beforeSaveCell: onBeforeSaveCell, // a hook for before saving cell
afterSaveCell: onSaveCell // a hook for after saving cell
};
const APPROVALCHAIN_QUERY = gql`
{
vApprovalChainApproverCountList{
applicationId
applicationName
collectionName
licenseType
cost
approvers
}
}
`;
const ADDCOST_MUTATION = gql`
mutation updateCostlicense($costLicense: ApplicaitonCostInput!){
updateCostLicense(costLicense: $costLicense){
applicationId
cost
licenseType
}
}
`;
class ApprovalRecord2 extends Component {
render() {
return (
<Query
query={APPROVALCHAIN_QUERY}
>
{({ loading, error, data }) => {
if (loading)
return <p>Loading...</p>;
if (error)
return <p>{error.message}</p>;
const chain = JSON.parse(JSON.stringify(data.vApprovalChainApproverCountList));
console.log(chain);
return (
<div>
<h1>ApprovalRecord2</h1>
<p>Add/Remove Approvers</p>
<BootstrapTable data={chain} striped hover pagination search options={options} cellEdit={cellEditProp} version='4'>
<TableHeaderColumn isKey dataField='applicationId' dataSort={true}>ID</TableHeaderColumn>
<TableHeaderColumn dataField='applicationName' dataSort={true}>Application</TableHeaderColumn>
<TableHeaderColumn dataField='collectionName' dataSort={true}>Collection</TableHeaderColumn>
<TableHeaderColumn dataField='licenseType' dataSort={true}>License</TableHeaderColumn>
<TableHeaderColumn dataField='cost' dataSort={true}>Cost</TableHeaderColumn>
<TableHeaderColumn dataField='approvers' dataSort={true}>Approvers</TableHeaderColumn>
</BootstrapTable>
)}
</div>
);
}}
</Query>
);
}
}
export default ApprovalRecord2;
You can pass/inject (multiple, named) mutations (and queries) as props to component with graphql()
I'm writing an image picker using react-native's CameraRoll API and rendering them in a FlatList inside CameraRollScreen component. This component takes a prop called maxPhotos, say 3, when a user has selected 3 photos, all other photos will be disabled (cannot be selected anymore), it looks like this (this is what I have right now, it works, but not performant):
As you can see, when I've selected 3 photos (which is the limit), all other photos are covered by a transparent view (disabled). This is not performant, doesn't seem so in the GIF, but when running on a real device, this problem can no longer be ignored. Selecting the first 2 photos doesn't cause any lag, however, upon selecting the last photo, since all other photos will have to be disabled, it becomes laggy. But I have no idea how else I could disable the other photos without disabling them 1 by 1. Here is the code I have for my image picker:
Since every image has different states, I also make each photo a PureComponent called CameraRollImage that has the following state:
{
uri: '',
index: -1 // if not selected, it's -1, if selected, it denotes
// the position of the photo in the 'selectedPhotos'
// array
disabled: false // Whether it should be disabled
}
CameraRollImage component:
class CameraRollImage extends PureComponent {
constructor(props) {
super(props);
this.state = {
uri: '',
index: -1,
disabled: false
};
this.onSelectPhoto = this.onSelectPhoto.bind(this);
}
componentWillMount() {
const { uri, index, disabled } = this.props;
this.setState({ uri, index, disabled });
}
componentWillReceiveProps(nextProps) {
const { uri, index, disabled } = nextProps;
this.setState({ uri, index, disabled });
}
onSelectPhoto() {
const { uri, index } = this.state;
this.props.onSelectPhoto({ uri, index });
// 'onSelectPhoto' is a method passed down to each photo
// from 'CameraRollScreen' component
}
render() {
const { uri, index, disabled } = this.state;
return (
<View style={{ ... }}>
<TouchableOpacity
disabled={disabled}
onPress={this.onSelectPhoto}
>
<Image
source={{ uri }}
style={{ ... }}
/>
</TouchableOpacity>
// If disabled, render a transparent view that covers the photo
{disabled && <View
style={{
position: 'absolute',
backgroundColor: 'rgba(0, 0, 0, 0.75)',
width: ... height: ...
}}
/>}
// render the index here
</View>
);
}
}
export default CameraRollImage;
Then, in CameraRollScreen Component:
class CameraRollScreen extends Component {
constructor(props) {
super(props);
this.state = {
allPhotos: [], // all photos in camera roll
selectedPhotos: []
};
this.onSelectPhoto = this.onSelectPhoto.bind(this);
this.renderPhoto = this.renderPhoto.bind(this);
}
componentWillMount() {
// Access the photo library to grab all photos
// using 'CameraRoll' API then push all photos
// to 'allPhotos' property of 'this.state'
}
onSelectPhoto({ uri, index }) {
let { selectedPhotos } = { ...this.state };
if (index === -1) {
// this means that this photo is not selected
// and we should add it to 'selectedPhotos' array
selectedPhotos.push(uri);
} else {
_.pullAt(selectedPhotos, index);
}
this.setState({ selectedPhotos });
}
renderPhoto({ item }) {
// item is the uri of the photo
const { selectedPhotos } = this.state;
const index = _.indexOf(selectedPhotos, item);
// A photo should be disabled when reach the limit &&
// it's not selected (index === -1)
return (
<CameraRollImage
uri={item}
index={index}
onSelectPhoto={this.onSelectPhoto}
disabled={index === -1 && selectedPhotos.length >= 3}
/>
);
}
render() {
const { allPhotos } = this.state;
return (
<FlatList
data={allPhotos}
extraData={this.state}
...
...
numColumns={3}
renderItem={this.renderPhoto}
/>
);
}
}
export default CameraRollScreen;
I have only 100 photos in my photo library and it's already causing lags, many people have way way way more photos than I do, this way will cause disaster, but how should I go about updating so many photos in FlatList? Or, should I use FlatList at all?
Found the solution, thanks to Pir Shukarullah Shah and RaphaMex.
If I scroll down fast enough, many images were not rendered and they are being rendered when I reach them. This seems right, why render them anyway when they're not on the screen? What I did was that I made use of onViewableItemsChanged of FlatList:
<FlatList
...
...
keyExtractor={(item) => item} // This is important!!!
onViewableItemsChanged={this.onViewablePhotosChanged}
initialNumberToRender={Math.ceil(SCREEN_HEIGHT / IMAGE_SIZE) * 3}
...
/>
Then, onViewablePhotosChanged method:
onViewablePhotosChanged({ viewableItems }) {
let viewablePhotos = [];
viewableItems.forEach((item) => viewablePhotos.push(item.key));
this.setState({ viewablePhotos });
// Here, every object in 'viewableItems' has a key, which
// is the key you provided in 'keyExtractor={(item) => ...}',
// I used the 'uri' of each photo as the key, that's why
// I am pushing viewable photos' uri's to 'viewablePhotos' array
}
Lastly, modify the renderPhoto function to pass a viewable prop
renderPhoto({ item }) {
...
...
return (
<CameraRollImage
...
...
viewable={_.include(this.state.viewablePhotos, item)}
/>
);
}
Then, in CameraRollImage component, where we render images, there is a prop called viewable, if viewable === false, we simply do not update it:
componentWillReceiveProps(nextProps) {
const { ..., ..., viewable } = nextProps;
if (!viewable) {
this.setState({ viewable: false });
return;
}
...
...
}
BETTER YET!!! if viewable is false, instead of rendering the image, we render an equal-sized empty view, you know, to save memory, which of course doesn't seem to be important if there're only 100 photos:
render() {
if (!this.state.viewable) {
return (
<View
style={{
width={IMAGE_SIZE}
height={IMAGE_SIZE}
}}
/>
);
}
return (
<Image
...
...
/>
);
}
I have the following setup in my React Project:
export default class OverviewScreen extends React.Component<any, any> {
public render() {
return (
<QueryRenderer
environment={environment}
query={OverviewScreenQuery}
render={this.queryRender}/>
);
}
protected queryRender({error, props}): JSX.Element {
if (error) {
return <div>{error.message}</div>;
} else if (props) {
return (
<div>
<div>
<ActivityOfferList viewer={props.viewer} title="Titel"/>
<ActivityTypeListsFragment viewer={props.viewer}/>
</div>
</div>
);
}
return <div>Loading...</div>;
}
}
const OverviewScreenQuery = graphql`
query OverviewScreenQuery {
viewer {
...HorizontalOfferList_viewer
...ActivityTypeLists_viewer
}
}`;
class ActivityTypeLists extends React.Component<IHorizontalOfferListProps, any> {
public render() {
return (
<div>
{this.props.viewer.allActivityTypes.edges.map((typeEdge) => {
let typeNode = typeEdge.node;
return this.getCardListForActivityType(typeNode);
})}
</div>
);
}
private getCardListForActivityType(typeNode: any) {
console.log(typeNode);
return (
<CardList key={typeNode.__id} title={typeNode.title}>
{typeNode.activities.edges.map(({node}) => {
return (
<RelayPicturedTypeActivityCard key={node.__id} offer={node} activityType={typeNode}/>
);
})}
</CardList>
);
}
}
export const ActivityTypeListsFragment = createFragmentContainer(ActivityTypeLists, graphql`
fragment ActivityTypeLists_viewer on Viewer {
allActivityTypes(first: 5) {
edges {
node {
...PicturedTypeActivityCard_offer
}
}
}
}
`);
export class PicturedTypeActivityCard extends React.Component<any, any> {
public render() {
return (
<PicturedCard title={this.props.offer.title} subtitle={this.props.activityType.title} width={3}/>
);
}
}
export const RelayPicturedTypeActivityCard = createFragmentContainer(PicturedTypeActivityCard, graphql`
fragment PicturedTypeActivityCard_offer on ActivityType {
title
activities(first: 4) {
edges {
node {
id
title
}
}
}
}
`);
Which should work and give me the correct result from the graphcool relay endpoint.
The Network call to the relay endpoint is indeed correct and I receive all the ActivityTypes and their activities and titles from my endpoint.
But somehow in the function getCardListForActivityType() the typeNode only contains the __id of the node as data and no title at all:
If I insert title and activities directly instead of using
...PicturedTypeActivityCard_offer
then the data also gets passed down correctly. So something with the Fragment must be off.
Why is it that the network call is complete and uses the fragment correctly to fetch the data, but the node object never gets the fetched data?
This is indeed correct behavior.
Your components must, individually, specify all their own data dependencies, Relay will only pass to the component the data it asked for. Since your component is not asking any data, it's receiving an empty object.
That __id you see is used internally by Relay and you should not rely on it (that is why it has the __ prefix).
Basically, the prop viewer on ActivityTypeLists component will have exactly the same format than the query requested on the ActivityTypeLists_viewer fragment, without any other fragments from other components that you are referencing there.
This is known as data masking, see more in the following links:
https://facebook.github.io/relay/docs/en/thinking-in-relay.html#data-masking
https://facebook.github.io/relay/docs/en/graphql-in-relay.html#relaymask-boolean
I have a reactjs component with redux which passes asynchronously props to child component.
In child component I try to catch the data in componentDidMount but somehow does not work either, however the child component is getting rendered.
This is my parent component
import React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import * as slidesActions from '../../actions/slidesActions';
import Slider from '../Partials/Slider'
import _ from 'underscore';
class HomePage extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.actions.getSlides()
}
componentWillMount() {
const {slides} = this.props;
}
render() {
const {slides} = this.props;
return (
<div className="homePage">
<Slider columns={1} slides={slides} />
</div>
);
}
}
function mapStateToProps(state) {
return {
slides: state.slides
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(slidesActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(HomePage);
here comes my child component where I try to get passed slides props but is empty
import React from 'react';
import _ from 'underscore';
import Hammer from 'hammerjs';
class Slider extends React.Component {
constructor(props) {
super(props)
this.updatePosition = this.updatePosition.bind(this);
this.next = this.next.bind(this);
this.prev = this.prev.bind(this);
this.state = {
images: [],
slidesLength: null,
currentPosition: 0,
slideTransform: 0,
interval: null
};
}
next() {
const currentPosition = this.updatePosition(this.state.currentPosition - 10);
this.setState({ currentPosition });
}
prev() {
//TODO: work on logic
if( this.state.currentPosition !== 0) {
const currentPosition = this.updatePosition(this.state.currentPosition + 10);
this.setState({currentPosition});
}
}
componentDidMount() {
//here I try set a state variable on slides
let {slides} = this.props
let slidesLength = slides.length
this.setState({slidesLength})
this.hammer = Hammer(this._slider)
this.hammer.on('swipeleft', this.next);
this.hammer.on('swiperight', this.prev);
}
componentWillUnmount() {
this.hammer.off('swipeleft', this.next)
this.hammer.off('swiperight', this.prev)
}
updatePosition(nextPosition) {
const { visibleItems, currentPosition } = this.state;
return nextPosition;
}
render() {
let {slides, columns} = this.props
let {currentPosition} = this.state
let sliderNavigation = null
//TODO: this should go to slides actions
let slider = _.map(slides, function (slide) {
let Background = slide.featured_image_url.full;
if(slide.status === 'publish')
return <div className="slide" id={slide.id} key={slide.id}><div className="Img" style={{ backgroundImage: `url(${Background})` }} data-src={slide.featured_image_url.full}></div></div>
});
if(slides.length > 1 ) {
sliderNavigation = <ul className="slider__navigation">
<li data-slide="prev" className="" onClick={this.prev}>previous</li>
<li data-slide="next" className="" onClick={this.next}>next</li>
</ul>
}
return <div ref={
(el) => this._slider = el
} className="slider-attached"
data-navigation="true"
data-columns={columns}
data-dimensions="auto"
data-slides={slides.length}>
<div className="slides" style={{ transform: `translate(${currentPosition}%, 0px)`, left : 0 }}> {slider} </div>
{sliderNavigation}
</div>
}
}
export default Slider;
and here I have my actions for slider
import * as types from './actionTypes';
import axios from 'axios';
import _ from 'underscore';
//TODO: this should be accessed from DataService
if (process.env.NODE_ENV === 'development') {
var slidesEndPoint = 'http://dev.server/wp-json/wp/v2/slides';
} else {
var slidesEndPoint = 'http://prod.server/wp-json/wp/v2/slides';
}
export function getSlides () {
return dispatch => {
dispatch(setLoadingState()); // Show a loading spinner
axios.get(slidesEndPoint)
.then(function (response) {
dispatch(setSlides(response.data))
dispatch(doneFetchingData(response.data))
})
/*.error((response) => {
dispatch(showError(response.data))
})*/
}
}
function setSlides(data) {
return {
type: types.SLIDES_SUCCESS,
slides: data
}
}
function setLoadingState() {
return {
type: types.SHOW_SPINNER,
loaded: false
}
}
function doneFetchingData(data) {
return {
type: types.HIDE_SPINNER,
loaded: true,
slides: data
}
}
function showError() {
return {
type: types.SHOW_ERROR,
loaded: false,
error: 'error'
}
}
Reason is, componentDidMount will get called only once, just after the initial rendering, since you are fetching the data asynchronously so before you get the data Slider component will get rendered.
So You need to use componentwillreceiveprops lifecycle method.
componentDidMount:
componentDidMount() is invoked immediately after a component is
mounted. Initialization that requires DOM nodes should go here. If you
need to load data from a remote endpoint, this is a good place to
instantiate the network request. Setting state in this method will
trigger a re-rendering.
componentWillReceiveProps:
componentWillReceiveProps() is invoked before a mounted component
receives new props. If you need to update the state in response to
prop changes (for example, to reset it), you may compare this.props
and nextProps and perform state transitions using this.setState() in
this method.
Write it like this:
componentWillReceiveProps(nextProps){
if(nextProps.slides){
let {slides} = nextProps.props
let slidesLength = slides.length;
this.hammer = Hammer(this._slider)
this.hammer.on('swipeleft', this.next);
this.hammer.on('swiperight', this.prev);
this.setState({slidesLength})
}
}
As far as I understand, you are doing an axios call to fetch the data and then set it in the reducer which you are returning later. Also initially reducer data is empty . Now since componentDidMount is called only once, and initially no data may have been there you are not seeing any values. Use a componentWillReceiveProps function
componentWillReceiveProps(nextProps) {
//here I try set a state variable on slides
let {slides} = nextProps
let slidesLength = slides.length
this.setState({slidesLength})
this.hammer = Hammer(this._slider)
this.hammer.on('swipeleft', this.next);
this.hammer.on('swiperight', this.prev);
}