TL;DR:
After reading through various resources about React and Relay and by building a prototype app with react-relay, I came across a problem that I cannot solve without help. It's about how to tell a mutation which data to save.
I have a component that shows details of a User entity. It's loaded via a relay query. This component has an <input /> field with the name of the user and a save button that triggers a relay.commitUpdate(...) call. Whenever I change the name of the user in the input field and click on save, I can see that not the changed but the original name is send to the backend.
I am building an app with react-relay and I am working on a component that should display details about a user entity with the possibility to change these data and save it. It`s the detail part of the Master-Detail-Pattern.
Loading and changing of data works fine. But changes are not commited to the backend. For data updates I am trying to use relay mutations.
After clicking on save, I can see in the developer console in my chrome browser, that unaltered data is send to my backend. What do I miss? Here is what I have so far (Typescript code).
(Update: when placing break points into the onSaveClick() method of the component and the getVariables() method in the mutation, I see that in onSaveClick the property this.props.user contains made changes while this.props.user in getVariables() returns the initial data.)
export interface IUserDetailsProps { user: User }
export interface IUserDetailsStates { user: User }
class UserDetails extends React.Component<IUserDetailsProps, IUserDetailsStates> {
constructor(props: IUserDetailsProps) {
super(props);
this.state = { user: this.props.user };
}
public onFirstNameChange(event: any) {
let user = this.state.user;
this.state.user.firstName = event.target.value;
this.props.user.firstName = event.target.value; // is this needed?
this.setState({ user: user });
}
public onSaveClick() {
this.props.relay.commitUpdate(new UserMutation({ user: this.props.user }));
}
public render(): JSX.Element {
let user: User = this.props.user;
return <div>
<input type="text" value={user.firstName} onChange={this.onFirstNameChange.bind(this)} />
<button onClick={this.onSaveClick}>Save</button>
</div>;
}
}
class UserMutation extends Relay.Mutation {
public getMutation() {
return Relay.QL`mutation {saveUser}`;
}
public getVariables() {
return this.props.user;
}
public getFatQuery() {
return Relay.QL`
fragment on UserPayload {
user {
id,
firstName
}
}
`;
}
public getConfigs() {
return [{
type: "FIELDS_CHANGE",
fieldIDs: {
user: this.props.user.id
}
}];
}
static fragments = {
user: () => Relay.QL`
fragment on User {
id,
firstName
}
`
}
}
export default Relay.createContainer(UserDetails, {
fragments: {
user: () => Relay.QL`
fragment on User {
id,
firstName,
${UserMutation.getFragment("user")}
}
`
}
});
Solution attempt:
I changed the line for the input field of the user's first name to this:
<input type="text"
ref={(ref) => {
this.myTextBox = ref;
ref.value = this.props.user.firstName;
}} />
and removed the change event handler and removed all state code.
My click handler now looks like this:
public onSaveClick() {
let user = Object.assign({}, this.props.user);
user.firstName = this.myTextBox.value;
this.props.relay.commitUpdate(new UserMutation({user: user}));
}
and I can see, that the user object passed to the mutation now has the new value for firstName that I have changed in the text input. But again, relay sends the unchanged user to my backend. Here is a screenshot that shows my problem:
Related
I am trying to render a simple profile from my redux store. Actually, the data is successfully provided and mapped to my props. I can log the profile information in the console. But when I render the profile data, it tells me that the data is not defined.
When I console log the profile , it says undefined first and then displays the data (see down). I wonder if react tries to render in the moment the data is undefined.
Console Log
undefined profile.js:28
Object profile.js:28
bio: "Test"
id: 2
image: "http://localhost:8000/media/default.jpg"
Component
export class ProfileDetail extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.getProfile(this.props.match.params.id);
}
static propTypes = {
profile: PropTypes.array.isRequired,
getProfile: PropTypes.func.isRequired,
};
render() {
const profile = this.props.SingleProfile;
console.log (profile)
return (
<Fragment>
<div className="card card-body mt-4 mb-4">
<h2>{profile.bio}</h2> // if i use here just a string its successfully rendering
</div>
</Fragment>
);
}
}
function mapStateToProps(state, ownProps) {
const { profile} = state.profile
const id = parseInt(ownProps.match.params.id, 10)
const SingleProfile = profile.find(function(e) {
return e.id == id;
});
return { profile, SingleProfile}
}
export default connect(
mapStateToProps,
{ getProfile}
)(ProfileDetail);
Im a little bit lost and happy for any clarification.
You need to check if profile is loaded.
It is normal that request takes some time - it is not instant - to load any data.
Check that profile exists and display its property
<h2>{profile && profile.bio}</h2>
As an alternative, you could display a message:
<h2>{profile ? profile.bio : "Profile not loaded yet"}</h2>
I have this component
const SummaryBar = props => {
const { MainObject} = props;
const localGetUserFromID = userID => {
getEmailFromId(userID).then(results => {
return results.data.Title; //Comment: This one returning friendly name
});
};
return (<span>Hello {localGetUserFromID(MainObject.AuthorId)}</span>)
}
but when I render it somehow the its only showing Hello and not the output I am getting from my localGetUserFromID function. Am I doing wrong? Note the AuthorId is being pass to an API and the MainObject came from the App Level,
FYI when I try to debug it using dev tools the function is retuning the text I am look for.
localGetUserFromID() returns nothing, that is, undefined, and that's why you see Hello only.
And because localGetUserFromID() makes an asynchronous call to get an email from user ID, it doesn't have to be in render() method. Now this component is defined as a state-less component, but you can re-define it as a stateful component, call the getEmailFromId() in componentDidMount() life-cycle method, and use a return value as an internal state.
Then you can show a value of the internal state after Hello.
class SummaryBar extends Component {
// Skipping prop type definition.
constructor(props) {
super(props)
this.state = {
username: '',
}
}
componentDidMount() {
const { MainObject: { AuthorId } } = this.props
getEmailFromId(AuthorId).then((results) => {
this.setState({
username: results.data.title,
})
})
}
render() {
const { username } = this.state
return (
<span>
Hello { username }
</span>
)
}
}
When things run when debugging but not when running and you are using promises as you are, the 99% of the times is because promises hasn't been resolved when you print.
localGetUserFromID indeed returns a promise that resolves to the friendly name.
You can just prepend await to localGetUserFromID(MainObject.AuthorId) and rewrite you return as this:
return (<span>Hello {await localGetUserFromID(MainObject.AuthorId)}</span>)
From my ReactJS application, am submitting a JSONRequest to External system via Java Servlet and getting response from that Servlet. Those response i have to set it in my React component.
But, am having issue. That am not able to set the values for the very first response. If i manually refresh the page those values are setting properly. And then, if i submitting request for 2nd time with the same form values that time am able to render the response values without any manual refresh.
During very first time response of the form am unable to set the response value.
Please find my below code that am trying.
class Orders extends React.Component {
constructor(props) {
super(props);
this.publishToOracle = this.publishToOracle.bind(this);
}
publishToOracle(id) {
var item=this.state.item;
ajax({
url: 'api/publish_oracle',
method: 'POST',
data: { id: id }
},
(data) => {
if (data.error == null) {
alert('Order has been published to Oracle Successfully')
item.oracle_number=data.result.oracle_number;
this.setState({ view: 'Details', item: data.result});
console.log("Order Number-->"+JSON.stringify(this.state.item.oracle_number)); // Oracle Number is printing here every time. But not in the component.
} else if (data.error != null) {
alert(data.error);
}
});
}
render() {
if (this.state.view == 'Details') {
return (
<Details
item={ this.state.item }
publishToOracle={this.publishToOracle}
/>
);
}
}
}
class Details extends React.Component {
constructor(props) {
super(props);
this.state = { item: props.item };
}
handlePublishToOracle(e) {
var item = this.state.item;
alert("Publishing to Oracle");
e.preventDefault();
item["oracle_order_number"] = "";
item["oracle_response_message"]="";
item["oracle_response_status"]="";
this.props.publishToOracle(item.id);
}
handleActionButtons() {
if(this.state.action == "PublishToOracle") {
return (
<button
type="submit"
onClick={ this.handlePublishToOracle}
>
Publish To Oracle
</button>
);
}
}
render() {
return (
<select
name="action"
value={this.state.action}
onChange={this.handleAction}
>
<option value="PublishToOracle"> Publish To Oracle</option>
</select>
{ this.handleActionButtons() }
<input
name="oracle_number"
type="text"
maxLength="20"
value={ this.state.item.oracle_number }
/>
);
}
}
Actually, the functionality of the API is to get the response save it in my MongoDB and render in React UI.
This problem is due to the below elements are undefined for the very first time. so am creating those elements with empty value and once we get the response from the api, its settings those values in the created elements and its works fine.
item["oracle_order_number"] = "";
item["oracle_response_message"]="";
item["oracle_response_status"]="";
this.props.publishToOracleEBS(item);
This solution has been updated in my questions too.
So, this problem has been well documented, yet here I am, stuck again...
On server side, I have publish:
Meteor.publish('userData', function(){ if (!this.userId) { return null; }
return Meteor.users.find({}, {fields: {'profile': 1}}); });
On client side router subscriptions, I have:
this.register('userData', Meteor.subscribe('userData'));
And then in my client code, I have:
if (Meteor.userId()) {
var profile = Meteor.users.find(Meteor.userId()).profile;
console.log(profile); // I keep getting undefined...
I am not using the autopublish or insecure package.
My data in the mongodb collection looks like:
{"_id" : "...", profile: { "password" : { "network" : "...", "picture" : "none" } }
My error says:
Uncaught TypeError: Cannot read property 'password' of undefined
Thoughts?
The user profile is automatically published to the client, you don't need to write a custom publication to send it. That's why you should never store sensitive user information in the profile. You can get rid of that publication.
To access the profile in a component, all you need to do is wrap your component (or entire app) in a data container that sends the profile to the component. The reason you do this is that the container is reactive, so the component will load until the meteor user object is ready, then you can immediately access stuff stored in the profile.
Create container:
import { Meteor } from 'meteor/meteor';
import { createContainer } from 'meteor/react-meteor-data';
import { Navigation } from '/imports/ui/components/user-navigation/Navigation';
export default createContainer(() => {
const loading = !Meteor.user();
const user = Meteor.user();
return { loading, user };
}, Navigation);
Access profile in component:
import React, { Component } from 'react';
export class Navigation extends React.Component {
constructor(props) {
super(props);
}
render() {
const { user, loading } = this.props;
return (
loading ? <Loading /> :
<div className="navigation">
User's name is: { user.profile.firstname }
</div>
)
}
};
So, as it turns out, most of my code was correct, but as ghybs pointed out above, I still needed to wait for the user data. So, here is my working code:
// in publish.js on the server
Meteor.publish('userData', function(){ if (!this.userId) { return null; }
return Meteor.users.find({_id: this.userId}, {fields: {'profile': 1}}); });
// in routes.jsx using FlowRouter
// after importing all the module external data
FlowRouter.route('/',{subscriptions:function(){
this.register('getUser', Meteor.subscribe('userData'));
}, action() {
mount(...etc...
// Keep in mind that I am using TrackerReact NPM to handle re-renders...
// in my component
if (FlowRouter.subsReady("getUser")) {
// this returns an array with one object
var userObject = Meteor.users.find({_id: Meteor.userId()}).fetch()[0];
Try replacing
var profile = Meteor.users.find({_id: Meteor.userId()}).profile;
with
var profile = Meteor.users.findOne({_id: Meteor.userId()}).profile;
I think the issue is with the query
var profile = Meteor.users.find({_id: Meteor.userId()}).profile;
or this should work too
var profile = Meteor.user().profile;
I cannot for the life of me figure this out. I have a parent component that gets a 'layout' field from the database. That value determines which child component should be rendered. The parent component also needs to get a GraphQL fragment from the child component. The parent component looks like this:
class Page extends React.Component{
_setLayout(){
const Layout = this.props.viewer.post.layout.meta_value || 'DefaultLayout';
const isDefault = Layout === 'DefaultLayout';
const isPostList = Layout === 'PostList';
this.props.relay.setVariables({
page: this.props.page,
isDefault: isDefault,
isPostList: isPostList
})
}
componentWillMount(){
this._setLayout()
}
render(){
const { viewer, className } = this.props;
const { post } = viewer;
const Layout = Layouts[post.layout.meta_value] || Layouts['Default'];
return <Layout.Component viewer={viewer} page={this.props.page} condition={true} layout={Layout}/>
}
}
export default Relay.createContainer(Page, {
initialVariables: {
page: null,
isDefault: false,
isPostList: false,
},
fragments: {
viewer: (variables) => Relay.QL`
fragment on User {
${DefaultLayout.getFragment('viewer', {page: variables.page, condition: variables.isDefault})},
post(post_name: $page){
id
layout{
id
meta_value
}
}
}
`
}
});
The child component (in this example DefaultLayout) looks like this:
class DefaultLayout extends React.Component{
componentWillMount(){
this.props.relay.setVariables({
page: this.props.page,
condition: this.props.condition
})
}
render(){
const { viewer } = this.props;
if (viewer.post){
const { post_title, post_content, thumbnail } = viewer.post;
let bg = {
backgroundImage: "url('" + thumbnail + "')"
}
let heroClass = thumbnail ? "hero_thumbnail" : "hero";
return(
<PostContent content={post_content}/>
)
} else {
return <div>Loading...</div>
}
}
}
export default Relay.createContainer(DefaultLayout, {
initialVariables:{
page: null,
condition: false
},
fragments: {
viewer: () => Relay.QL`
fragment on User {
post(post_name:$page) #include(if: $condition){
id
post_title
post_content
thumbnail
},
settings{
id
uploads
amazonS3
}
}
`
}
});
When the initial query is run, condition is false so it doesn't get any post data. Right before DefaultLayout mounts, condition variable is set to true so it SHOULD query post and get that data.
Here is the weird thing: I'm only getting the id and post_title of post. I'm not getting post_content or thumbnail. If I set condition to be true in initial variables, I get everything.
Am I not supposed to be able to change the condition?
Lastly - here is a screenshot of the query that's generating. Its querying Page, not Routes, which seems wrong.
I think the problem is that you've not passed what you've expected to pass to LayoutComponent. Your page variable can be found in Relay's variables store, not on props:
return (
<Layout.Component
condition={true}
layout={Layout}
page={this.props.relay.variables.page}
viewer={viewer}
/>
);
Also, you can omit the componentWillMount step in DefaultLayout. page and condition should flow into variables from props automatically.
See also:
https://github.com/facebook/relay/blob/master/src/container/RelayContainer.js#L790-L807
https://github.com/facebook/relay/issues/309
https://github.com/facebook/relay/issues/866