Why does my data disappear when a subscription goes through? - reactjs

Why am I getting undefined for data when the subscription goes through?
I am trying to figure out where the undefined for my props.data.allLocations is coming from. Any help would be appreciated.
//Higher order component
export const LocationList = graphql(
gql`
query ($site: ID!) {
allLocations(
filter: {
site: {
id:$site
}
}
) {
id
name
}
}
`,
{
options: (ownProps)=>({
variables: {
site: ownProps.site
},
}),
//props were injected from the JSX element
props: props => {
return {
data: props.data,
subscribeToData: params => {
return props.data.subscribeToMore({
document:
gql`
subscription newLocations {
Location {
mutation
node {
id
name
}
}
}
`,
variables: {
//Empty for now
//site: props.site,
},
updateQuery: (previousState, {subscriptionData}) => {
if (!subscriptionData.data) {
return previousState;
}
var newArray =[subscriptionData.data.Location.node].concat(previousState.allLocations)
var newState = {
...previousState,
allLocations: [
{
...subscriptionData.data.Location.node
},
...previousState.allLocations
],
};
return newState
}
})
},
}
},
onError: (err) => console.error(err)
} )(EntityList)
//List Component
class EntityList extends Component {
componentWillReceiveProps(newProps) {
if (!newProps.data.loading) {
console.log(newProps)
if (this.subscription && this.props.hasOwnProperty('subscribeToData')) {
if (newProps.data.allLocations !== this.props.data.allLocations) {
console.log("Resubscribe")
// if the todos have changed, we need to unsubscribe before resubscribing
this.subscription()
} else {
console.log('else')
// we already have an active subscription with the right params
return
}
}
this.subscription = this.props.subscribeToData(newProps)
}}
render () {
if (this.props.data.loading) {
return (<div>Loading</div>)
}
var Entities = [];
for(var key in this.props.data) {
if(this.props.data[key] instanceof Array){
Entities = Entities.concat(this.props.data[key])
//console.log(this.props.data[key])
}
}
return (
<div className='w-100 flex justify-center bg-transparent db' >
<div className='w-100 db' >
{Entities.map((entity) =>
(
<EntityListView
user={this.props.user}
key={entity.id}
entityId={entity.id}
name={entity.name}
profilePic={(entity.profilePic)?entity.profilePic.uuid : this.props.defaultPic.uuid}
clickFunc={this.props.clickFunc}
/>
))}
</div>
</div>
)
}
}

Thanks to all who looked into this.
I got it to work by splitting up my queries as suggested by #nburk.
I was able to keep it modular by creating my own high order component.
If anyone is interested:
const Subscriber = (document, config) => {
return (WrappedComponent) => {
return class extends Component {
constructor(props) {
super(props)
this.state={}
this.subscription = null
}
componentWillReceiveProps(nextProps) {
console.log(nextProps)
if (!this.subscription && !nextProps.data.loading) {
console.log("Sucess")
let { subscribeToMore } = this.props.data
this.subscription = [subscribeToMore(
{
document: document,
updateQuery: (previousResult, { subscriptionData }) => {
console.log(subscriptionData)
var newResult = [subscriptionData.data[config.modelName].node].concat(previousResult[config.propName])
console.log(newResult)
return {
//"allItems"
[config.propName]: newResult,
}
},
onError: (err) => console.error(err),
}
)]
this.setState({});
}
}
render () {
if (this.props.data.loading) {
return (<div>Loading</div>)
}
return (
<WrappedComponent {...this.props} />
)
}
}
}
}
export default Subscriber
/*prop config = {
propName: "string", //allItems // from query
//so I do not have to parse the gql query (would have to recieve from parent)
modelName: "string" //Item
// from subscribe //so I do not have to parse the gql subscribe
}
*/
I now wrap my component like this:
export const LocationList = graphql(gql`...,
{options: (ownProps)=>({
variables: {
site: ownProps.site
}})})(
Subscriber(gql`...,
propName:"allLocations",
modelName:"Location",
)(EntityList))
Thanks

Related

how to create refs for content that gets created later

I have a component that fetches data from an api upon user input. This data then gets rendered onto the screen as <li/> tags. I want those <li/> tags to have a ref.
I tried creating an object of refs that I create after the data is fetched:
this.singleRefs = data.reduce((acc, value) => {
acc[value.id] = React.createRef();
return acc;
}, {});
and then later assign these refs to the <li/> tag: <li ref={this.singleRefs[element.id]}>
but when I print them out I always have {current:null} Here is a demo
what am I doing wrong?
With dynamic ref data, I'd propose that you should use callback refs.
import React from "react";
import "./styles.css";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: []
};
this.singleRefs = {};
}
componentDidMount() {
const data = [
{ value: "val1", id: 1 },
{ value: "val2", id: 2 },
{ value: "val3", id: 3 }
];
this.myFunc(data);
//you don't need this anymore
// this.singleRefs = data.reduce((acc, value) => {
// acc[value.id] = React.createRef();
// return acc;
// }, {});
}
myFunc = async (data) => {
await sleep(3000);
this.setState({ data });
};
renderContent() {
return this.state.data.map(
function (element, index) {
return (
<li key={index} ref={(node) => (this.singleRefs[element.id] = node)}>
{element.value}
</li>
);
}.bind(this)
);
}
render() {
console.log(this.singleRefs);
return <ul>{this.renderContent()}</ul>;
}
}
Sandbox

What's wrong with my method call I try to learn React and must pass component and Props to child

I learn Reactjs and javascript and wanted to call this withFetching Component but don't understand how to set the arguments correctly. I understand overall logic but still learning the details
Here is the switch where I call the withFetching
render() {
const theFile = encodeURI(`./images/${fileData}`);
switch (mediaType) {
case 'xlsx': {
const newProps = { ...this.props, responseType: 'arraybuffer' };
return (
<div className="pg-viewer-wrapper">
<div className="pg-viewer" id="pg-viewer">
<{withFetching(XlsxViewer, newProps, fileType="xlsx", filePath={theFile} )}/>
</div>
</div>
);
}
.........
I try like this also:(making WithFetching camel-case even it's a function)
return (
<div className="pg-viewer-wrapper">
<div className="pg-viewer" id="pg-viewer">
<WithFetching XlsxViewer={XlsxViewer} newProps={newProps} />
</div>
</div>
);
But the WithFetching constructor never firers!
I try like this:
case 'xlsx': {
const newProps = { ...this.props, responseType: 'arraybuffer', fileType: 'xlsx', filePath: { theFile } };
// return withFetching(XlsxViewer, newProps);
return (
<div className="pg-viewer-wrapper">
<div className="pg-viewer" id="pg-viewer">
{WithFetching(XlsxViewer, newProps)};
</div>
</div>
);
}
But still the WithFetching constructor never firers!
Error: (yea I know the way I use brackets are my mistake it's hard to learn)
And this is the withFetching that is in its own file called fetch-wrapper.jsx. The WrappedComponent argument is the above XlsxViewer that is the final "On-screen" Component!
import React, { Component } from 'react';
import Error from './error';
import Loading from './loading';
function withFetching(WrappedComponent, props) {
return class FetchComponent extends Component {
constructor(props) {
// eslint-disable-line no-shadow
super(props);
this.state = {};
this.xhr = this.createRequest(props.filePath);
}
componentDidMount() {
try {
this.fetch();
} catch (e) {
if (this.props.onError) {
this.props.onError(e);
}
this.setState({ error: 'fetch error' });
}
}
componentWillUnmount() {
this.abort();
}
createRequest(path) {
let xhr = new XMLHttpRequest();
if ('withCredentials' in xhr) {
// XHR for Chrome/Firefox/Opera/Safari.
xhr.open('GET', path, true);
// } else if (typeof XDomainRequest !== 'undefined') {
// // XDomainRequest for IE.
// xhr = new XDomainRequest();
// xhr.open('GET', path);
} else {
// CORS not supported.
xhr = null;
return null;
}
if (props.responseType) {
xhr.responseType = props.responseType;
}
xhr.onload = () => {
if (xhr.status >= 400) {
this.setState({ error: `fetch error with status ${xhr.status}` });
return;
}
const resp = props.responseType ? xhr.response : xhr.responseText;
this.setState({ data: resp });
};
return xhr;
}
fetch() {
this.xhr.send();
}
abort() {
if (this.xhr) {
this.xhr.abort();
}
}
render() {
if (!this.xhr) {
return <h1>CORS not supported..</h1>;
}
if (this.state.error) {
return <Error {...this.props} error={this.state.error} />;
}
if (this.state.data) {
return <WrappedComponent data={this.state.data} {...this.props} />;
}
return <Loading />;
}
};
}
export default withFetching;
And this the final XlxsViewer Component that will be visible.
Thanks to Copyright (c) 2017 PlanGrid, Inc.
import React, { Component } from 'react';
import XLSX from 'xlsx';
import CsvViewer from './csv-viewer';
class XlxsViewer extends Component {
constructor(props) {
super(props);
this.state = this.parse();
}
parse() {
const dataArr = new Uint8Array(this.props.data);
const arr = [];
for (let i = 0; i !== dataArr.length; i += 1) {
arr.push(String.fromCharCode(dataArr[i]));
}
const workbook = XLSX.read(arr.join(''), { type: 'binary' });
const names = Object.keys(workbook.Sheets);
const sheets = names.map(name => XLSX.utils.sheet_to_csv(workbook.Sheets[name]));
return { sheets, names, curSheetIndex: 0 };
}
renderSheetNames(names) {
const sheets = names.map((name, index) => (
<input
key={name}
type="button"
value={name}
onClick={() => {
this.setState({ curSheetIndex: index });
}}
/>
));
return <div className="sheet-names">{sheets}</div>;
}
renderSheetData(sheet) {
const csvProps = Object.assign({}, this.props, { data: sheet });
return <CsvViewer {...csvProps} />;
}
render() {
const { sheets, names, curSheetIndex } = this.state;
return (
<div className="spreadsheet-viewer">
{this.renderSheetNames(names)}
{this.renderSheetData(sheets[curSheetIndex || 0])}
</div>
);
}
}
export default XlxsViewer;

Redux with Immer not updating component

I'm trying to add an element to an array in an object in my Redux store. I see the object get added to the store but it is not updating the component. If I leave the page and return it is showing up.
I'm pretty sure this is a state mutation issue but I can't figure out where I'm going wrong unless I fundamentally misunderstand what Immer is doing. In the component I'm using produce to add the string to the array, passing the new object to my reducer and using produce to add that object to an array of those objects.
I've looked through a ton of similar questions that all relate to state mutation, but the way I understand it the return from the component's call to produce should be a fully new object. Then in the reducer the call to produce should be returning a new object array.
This is the first time using Immer in a large project so it's entirely possible I don't fully get how it's working it's magic.
Component
import produce from 'immer';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import uuid from 'uuid/v4';
import { generate } from 'generate-password';
import { updateLeague } from '../../actions/leagues';
import { addTeam } from '../../actions/teams';
import { addUser } from '../../actions/users';
import Team from '../../classes/Team';
import User from '../../classes/User';
import UserWidget from '../utils/user/UserWidget';
class ViewLeague extends Component {
state = {
league : null,
isOwner : false,
owner : '',
teams : [],
inviteEmail: ''
};
componentWillMount() {
console.log('mount props', this.props.leagues);
const { leagues, uuid, leagueId, users, teams } = this.props;
if (leagues.length > 0) {
const league = leagues.find(league => league.uuid === leagueId);
const owner = users.find(user => league.leagueManager === user.uuid);
const leagueTeams = teams.filter(team => league.teams.includes(team.uuid));
this.setState({
league,
isOwner: league.leagueManager === uuid,
owner,
teams : leagueTeams
});
}
}
handleUpdate(event, fieldName) {
this.setState({ [ fieldName ]: event.target.value });
}
findUserByEmail(email) {
//Todo if not found here hit server
return this.props.users.find(user => user.email === email);
}
sendInvite = () => {
const { addTeam, addUser, updateLeague } = this.props;
const { league } = this.state;
const newManager = this.findUserByEmail(this.state.inviteEmail);
const newTeamUuid = uuid();
let newLeague = {};
if (newManager) {
const newTeam = new Team('New Team', newManager.uuid, newTeamUuid);
addTeam(newTeam);
} else {
const newPass = generate({
length : 10,
number : true,
uppercase: true,
strict : true
});
const newUserUuid = uuid();
const newUser = new User('', this.state.inviteEmail, newPass, '', '', newUserUuid);
addUser(newUser);
const newTeam = new Team('New Team', newUserUuid, newTeamUuid);
addTeam(newTeam);
newLeague = produce(league, draft => {draft.teams.push(newTeamUuid);});
updateLeague(newLeague);
console.log('invite props', this.props);
console.log('league same', league === newLeague);
}
//Todo handle sending email invite send password and link to new team
console.log('Invite a friend', this.state.inviteEmail);
};
renderInvite() {
const { isOwner, league, teams } = this.state;
if (isOwner) {
if ((league.leagueType === 'draft' && teams.length < 8) || league.leagueType !== 'draft') {
return (
<div>
<p>You have an empty team slot. Invite a fried to join!</p>
<input type="text"
placeholder={'email'}
onChange={() => this.handleUpdate(event, 'inviteEmail')}/>
<button onClick={this.sendInvite}>Invite</button>
</div>
);
}
}
}
renderViewLeague() {
console.log('render props', this.props.leagues);
const { league, owner, teams } = this.state;
const editLink = this.state.isOwner ?
<Link to={`/leagues/edit/${this.props.leagueId}`}>Edit</Link> :
'';
return (
<div>
<h2>{league.leagueName} </h2>
<h3>League Manager: <UserWidget user={owner}/> - {editLink}</h3>
<p>League Type: {league.leagueType}</p>
{this.renderInvite()}
<br/>
<hr/>
<h2>Teams</h2>
<span>{teams.map((team) => (<p key={team.uuid}>{team.teamName}</p>))}</span>
<span>
<h2>Scoring: </h2>
{league.scoring.map((score, index) => (
<p key={index}>{`Round ${index + 1}: ${score} points`}</p>
)
)}
</span>
</div>
);
}
render() {
if (!this.state.league) {
return (
<div>
<h2>No league Found</h2>
</div>
);
} else {
return (
<div>
{this.renderViewLeague()}
</div>
);
}
}
}
export default connect(
({ leagues: { leagues }, teams: { teams }, users: { users }, auth: { uuid } },
{ match: { params: { leagueId } } }) => ({
leagues,
teams,
users,
uuid,
leagueId
}), ({
addTeam : (team) => addTeam(team),
addUser : (user) => addUser(user),
updateLeague: (league) => updateLeague(league)
})
)(ViewLeague);
Reducer
import produce from 'immer';
import {
ADD_LEAGUE,
UPDATE_LEAGUE
} from '../actions/types';
const DEFAULT_LEAGUES = {
leagues: [ {
leagueName : 'Test League',
leagueManager: 'testUser12345',
uuid : 'testLeague12345',
teams : [ 'testTeam12345', 'testTeam23456' ],
scoring : [ 25, 20, 15, 10, 5, -5 ],
leagueType : 'draft'
} ]
};
const leaguesReducer = (state = DEFAULT_LEAGUES, action) =>
produce(state, draft => {
// noinspection FallThroughInSwitchStatementJS
switch (action.type) {
case ADD_LEAGUE:
draft.leagues.push(action.league);
case UPDATE_LEAGUE:
console.log('updating league', action.league);
const { league } = action;
const leagueIndex = draft.leagues.findIndex(fLeague => league.uuid === fLeague.uuid);
draft.leagues.splice(leagueIndex, 1, league);
}
});
export default leaguesReducer;
Any help is greatly appreciated!! More info available if needed
Try adding return; at the end of your case blocks.
You can read more about returning data from producers and see examples of what to do and what not to do here.

Assigning state to props from redux does not work

import React, { Component } from 'react';
import DisplayTable from './Table.js';
class App extends Component {
constructor(props) {
super(props);
this.state = {
menuItems: this.props.menu_items,
searchString: '',
displayItems: this.props.menu_items
}
this.search = this.search.bind(this);
this.handleChange = this.handleChange.bind(this);
}
componentWillMount() {
this.props.get_menu_items_api(false);
}
componentWillReceiveProps(nextProps) {
this.setState({ menuItems: nextProps.menu_items })
}
handleChange(e, isEnter) {
const searchData = () => {
let tempMenuProductDetails = this.props.menu_items;
const filterArray = tempMenuProductDetails.reduce((result, category) => {
if (category.categoryName.toLowerCase()
.indexOf(this.state.searchString.toLowerCase()) > -1) {
result.push(category);
}
if (category.productList && category.productList.length > 0) {
category.productList = category.productList.reduce((productListResult,
productList) => {
if (!!productList.productName &&
productList.productName.toLowerCase()
.indexOf(this.state.searchString.toLowerCase()) > -1)
{
productListResult.push(productList);
}
return productListResult;
}, []);
}
return result;
}, []);
this.setState({
displayItems: filterArray
}, function () {
console.log(this.state.displayItems);
})
console.log(filterArray);
}
if (!isEnter) {
this.setState({
searchString: e.target.value
});
} else {
searchData();
}
}
search(e) {
if (e.keyCode == 13) {
this.handleChange(e, true);
}
this.handleChange(e, false);
}
render() {
console.log(this.state.displayItems);
console.log(this.props.menu_items);
console.log(this.state.menuItems);
return (
<DisplayTable dataProp={this.state.displayItems} editFuncProp=
{this.props.edit_menu_items_api} /> )
}
}
export default App;
I have this search function in this file that does not update the value of props coming from the container of redux. Now when I pass {this.state.displayItems} in menu ,it does not display the data.
But when I pass {this.props.menu_items} it displays the data and I am not able to modify this.props.menu_items on the basis of search.
I have tried this code . what should i do?
The problem seems to be that, initially this.props.menu_items is an empty array and only after some API call the value is updated and you get the returned array on the second render, thus if you use it like
<DisplayTable dataProp={this.props.menu_items} editFuncProp=
{this.props.edit_menu_items_api} />
it works. Now that you use
<DisplayTable dataProp={this.state.displayItems} editFuncProp=
{this.props.edit_menu_items_api} />
and displayItems is only initialized in the constructor which is only executed once at the time, component is mounted and hence nothing is getting displayed.
The solution seems to be that you update the displayItems state in componentWillReceiveProps and call the search function again with the current search string so that you search results are getting updated.
Code:
import React, { Component } from 'react';
import DisplayTable from './Table.js';
class App extends Component {
constructor(props) {
super(props);
this.state = {
menuItems: this.props.menu_items,
searchString: '',
displayItems: this.props.menu_items
}
this.search = this.search.bind(this);
this.handleChange = this.handleChange.bind(this);
}
componentWillMount() {
this.props.get_menu_items_api(false);
}
componentWillReceiveProps(nextProps) {
this.setState({ menuItems: nextProps.menu_items, displayItems: nextProps.menu_items })
this.handleChange(null, true);
}
handleChange(e, isEnter) {
const searchData = () => {
let tempMenuProductDetails = this.props.menu_items;
const filterArray = tempMenuProductDetails.reduce((result, category) => {
if (category.categoryName.toLowerCase()
.indexOf(this.state.searchString.toLowerCase()) > -1) {
result.push(category);
}
if (category.productList && category.productList.length > 0) {
category.productList = category.productList.reduce((productListResult,
productList) => {
if (!!productList.productName &&
productList.productName.toLowerCase()
.indexOf(this.state.searchString.toLowerCase()) > -1)
{
productListResult.push(productList);
}
return productListResult;
}, []);
}
return result;
}, []);
this.setState({
displayItems: filterArray
}, function () {
console.log(this.state.displayItems);
})
console.log(filterArray);
}
if (!isEnter) {
this.setState({
searchString: e.target.value
});
} else {
searchData();
}
}
search(e) {
if (e.keyCode == 13) {
this.handleChange(e, true);
}
this.handleChange(e, false);
}
render() {
console.log(this.state.displayItems);
console.log(this.props.menu_items);
console.log(this.state.menuItems);
return (
<DisplayTable dataProp={this.state.displayItems} editFuncProp=
{this.props.edit_menu_items_api} /> )
}
}
export default App;

Infinite-loop in Redux and one more error

I have a table (10X10) which renders random values. If I push the arrow (up and down) in header, the column must be changed to increasing or reducing column. After that all rows must be changed.
My code:
App.jsx
class App extends React.Component {
constructor() {
super();
const dataCompleted = generateNewData(10, dataColumnNames.columnNames.length,
dataColumnNames.columnNames);
this.store = Redux.createStore(
appReducers,
{
table: {
header: dataColumnNames.columnNames,
data: dataCompleted,
footer: generateNewFooter(dataCompleted)
},
sorting: {
sortType: null,
columnId: null,
test: null
},
showDetails: {
rowId: null
}
});
}
render() {
return <Provider store={this.store}>
<div className="container">
<Table />
</div>
</Provider>
}
}
export default App;
Table.jsx
class TableBlock extends React.Component {
constructor() {
super();
}
componentDidUpdate(){
if (this.props.sorting.sortType === 1) {
let newData1 = (oldData) => {
let newData = [];
for (let i = 0; i < oldData.length; i++) {
newData[i] = oldData[i].sort(function(a, b) {
return a + b;
});
}
return newData;
}
this.props.dispatchNewData(newData1(this.props.data));
} else {
let newData1 = (oldData) => {
let newData = [];
for (let i = 0; i < oldData.length; i++) {
newData[i] = oldData[i].sort(function(a, b) {
return a - b;
});
}
return newData;
}
this.props.dispatchNewData(newData1(this.props.data));
}
}
renderBody() {
let result = this.props.table.data.map(function (dataItem, index) {
return <Body content={dataItem} key={index} />
});
return result;
}
render() {
return <table className="table">
<Header />
<tbody>{this.renderBody()}</tbody>
<Footer />
</table>
}
}
const mapStateToProps = function(state = {}) {
return {
table: state.table,
data: state.table.data,
sorting: state.sorting,
columnId: state.sorting.columnId,
showDetails: state.showDetails
}
}
const mapDispatchToProps = function(dispatch) {
return {
dispatchNewData: function(data) {
dispatch(table(data));
}
}
}
const Table = connect(mapStateToProps, mapDispatchToProps)(TableBlock);
export default Table;
Reducer - table.js
import { TABLE } from '../constants/table';
export function table(state = {}, action) {
switch (action.type) {
case TABLE:
return {
state: state,
data: action.data
};
default:
return state
}
}
Action - table.js
import { TABLE } from '../constants/table’;
export function table(data) {
return {
type: TABLE,
data
}
}
My errors:
props.table.data - isn’t changing
if I push 3 or more time the button - appearing infinity loop
after the first pushing - appearing error:
TypeError: undefined is not an object (evaluating 'this.props.content.map')
I think object props is changing. It is not good.

Resources