I'm trying to get mobx to work with my react hooks and I am starting off with a very simple example but it still does not work. I must have missed something but I have been trying for hours to find what that might be.
Here is my store:
import { observable, decorate } from 'mobx';
import { createContext } from 'react';
import { IUser } from '../models/IUser';
export class UserStore {
public user: IUser;
public getUser() {
myApi
.get(myUrl)
.then(response => {
this.user = response;
});
}
}
decorate(UserStore, {
user: observable,
});
export default createContext(new UserStore());
And here is the component printing the username of the user:
import React, { useContext } from 'react';
import { observer } from 'mobx-react-lite';
import UserStore from '../../stores/UserStore';
const MyComponent = observer(() => {
const userStore = useContext(UserStore);
return (
<div>
{userStore.user && userStore.user.userName}
</div>
);
});
export default MyComponent;
And to fire the api call, App does the following:
const App: React.FC = () => {
const userStore = useContext(UserStore);
useEffect(() => {
userStore.getUser();
}, []);
return(...);
};
export default App;
I can see that
1: App performs the call
2: The user is set to the response of the call
3: If I console log the userStore.user.userName after it has been set, it looks just fine.
The quirk is that the label in MyComponent never gets updated. Why?
I believe the bug is in decorate.
Changing the behavior from using the decorate syntax, to wrapping the FC with observable works just fine, like this:
const UserStore = observable({
user: {}
});
Another thing that also works is to have your stores as classes and using the old decorator syntax like this:
export class UserStore {
#observable public user: IUser;
}
This requires you to add the following to .babelrc:
["#babel/plugin-proposal-decorators", { "legacy": true }]
Related
Following this react-firestore-tutorial
and the GitHub code. I wonder if the following is correct way to use the onAuthStateChanged or if I have understod this incorrect I'm just confused if this is the right way.
CodeSandBox fully connect with a test-account with apikey to Firebase!! so you can try it what I mean and I can learn this.
(NOTE: Firebase is blocking Codesandbox url even it's in Authorised domains, sorry about that but you can still see the code)
t {code: "auth/too-many-requests", message: "We have blocked all
requests from this device due to unusual activity. Try again later.",
a: null}a:
Note this is a Reactjs-Vanilla fully fledge advanced website using only;
React 16.6
React Router 5
Firebase 7
Here in the code the Firebase.js have this onAuthStateChanged and its called from two different components and also multiple times and what I understand one should only set it up once and then listen for it's callback. Calling it multiple times will that not create many listeners?
Can someone have a look at this code is this normal in Reactjs to handle onAuthStateChanged?
(src\components\Firebase\firebase.js)
import app from 'firebase/app';
import 'firebase/auth';
import 'firebase/firestore';
class Firebase {
constructor() {
app.initializeApp(config);
.......
}
.....
onAuthUserListener = (next, fallback) =>
this.auth.onAuthStateChanged(authUser => {
if (authUser) {
this.user(authUser.uid)
.get()
.then(snapshot => {
const dbUser = snapshot.data();
// default empty roles
if (!dbUser.roles) {
dbUser.roles = {};
}
// merge auth and db user
authUser = {
uid: authUser.uid,
email: authUser.email,
emailVerified: authUser.emailVerified,
providerData: authUser.providerData,
...dbUser,
};
next(authUser);
});
} else {
fallback();
}
});
user = uid => this.db.doc(`users/${uid}`);
}
export default Firebase;
This two rect-higher-order Components:
First withAuthentication:
(src\components\Session\withAuthentication.js)
import React from 'react';
import AuthUserContext from './context';
import { withFirebase } from '../Firebase';
const withAuthentication = Component => {
class WithAuthentication extends React.Component {
constructor(props) {
super(props);
this.state = {
authUser: JSON.parse(localStorage.getItem('authUser')),
};
}
componentDidMount() {
this.listener = this.props.firebase.onAuthUserListener(
authUser => {
localStorage.setItem('authUser', JSON.stringify(authUser));
this.setState({ authUser });
},
() => {
localStorage.removeItem('authUser');
this.setState({ authUser: null });
},
);
}
componentWillUnmount() {
this.listener();
}
render() {
return (
<AuthUserContext.Provider value={this.state.authUser}>
<Component {...this.props} />
</AuthUserContext.Provider>
);
}
}
return withFirebase(WithAuthentication);
};
export default withAuthentication;
And withAuthorization:
(src\components\Session\withAuthorization.js)
import React from 'react';
import { withRouter } from 'react-router-dom';
import { compose } from 'recompose';
import AuthUserContext from './context';
import { withFirebase } from '../Firebase';
import * as ROUTES from '../../constants/routes';
const withAuthorization = condition => Component => {
class WithAuthorization extends React.Component {
componentDidMount() {
this.listener = this.props.firebase.onAuthUserListener(
authUser => {
if (!condition(authUser)) {
this.props.history.push(ROUTES.SIGN_IN);
}
},
() => this.props.history.push(ROUTES.SIGN_IN),
);
}
componentWillUnmount() {
this.listener();
}
render() {
return (
<AuthUserContext.Consumer>
{authUser =>
condition(authUser) ? <Component {...this.props} /> : null
}
</AuthUserContext.Consumer>
);
}
}
return compose(
withRouter,
withFirebase,
)(WithAuthorization);
};
export default withAuthorization;
This is normal. onAuthStateChanged receives an observer function to which a user object is passed if sign-in is successful, else not.
Author has wrapped onAuthStateChanged with a higher order function – onAuthUserListener. The HOF receives two parameters as functions, next and fallback. These two parameters are the sole difference when creating HOC's withAuthentication and withAuthorization.
The former's next parameter is a function which stores user data on localStorage
localStorage.setItem('authUser', JSON.stringify(authUser));
this.setState({ authUser });
while the latter's next parameter redirects to a new route based on condition.
if (!condition(authUser)) {
this.props.history.push(ROUTES.SIGN_IN);
}
So, we are just passing different observer function based on different requirements. The component's we will be wrapping our HOC with will get their respective observer function on instantiation. The observer function are serving different functionality based on the auth state change event. Hence, to answer your question, it's completely valid.
Reference:
https://firebase.google.com/docs/reference/js/firebase.auth.Auth#onauthstatechanged
https://reactjs.org/docs/higher-order-components.html
So, basically I have started bulding new app with React, Redux and Firebase.
I am still learning and am not able to solve some things but this is the first moment, when I am really stuck.
I have created a collection in the Firestore called 'posts' and created one post manually just to see if it is working.
Unfortunately, after implementing my code I keep receiving an error:
TypeError: Cannot read property 'posts' of undefined
AdminpanelAuth.componentDidMount
/src/components/Admin/Adminpanel.js:23
20 | componentDidMount() {
21 | this.setState({ loading: true });
22 |
> 23 | this.props.firebase.posts().on('value', snapshot => { <- HERE IS THE PROBLEM
I have tried different variations and different approaches but none of them seemed to be working. Could anyone advise? Below I am attaching a part of my code of Adminpanel.js and Firebase.js
Firebase.js
import app from 'firebase/app';
import 'firebase/auth';
import 'firebase/firestore';
const firebaseConfig = {
my config here
};
class Firebase {
constructor() {
app.initializeApp(firebaseConfig);
// initialize Firebase Authenticator
this.auth = app.auth();
// initialize Firebase Realtime Database
this.db = app.firestore();
}
// Initialize two functions that connect to Firebase : Log In and Log Out
doSignInWithEmailAndPassword = (email, password) =>
this.auth.signInWithEmailAndPassword(email, password);
doSignOut = () => this.auth.signOut();
// Initialize functions for posts
post = pid => this.db.doc('posts'+pid);
posts = () => this.db.collection('posts');
}
export default Firebase;
Adminpanel.js
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { compose } from 'recompose'
import { withFirebase } from '../Firebase'
import { withAuthorisation } from '../Session'
import SignOutBtn from '../SignOutBtn'
const Adminpanel = ({authUser}) => (
<div>{authUser ? <AdminpanelAuth /> : <AdminpanelNonAuth />}</div>
)
class AdminpanelAuth extends Component {
constructor(props) {
super(props);
this.state = {
loading:false,
posts:[]
}
}
componentDidMount() {
this.setState({ loading: true });
this.props.firebase.posts().on('value', snapshot => {
const postsObject = snapshot.val();
const postsList = Object.keys(postsObject).map(key => ({
...postsObject[key],
pid: key,
}));
this.setState({
posts: postsList,
loading: false,
});
});
}
./Firebase/index.js
import FirebaseContext, { withFirebase } from './context';
import Firebase from './Firebase';
export default Firebase;
export { FirebaseContext, withFirebase };
It looks as though your problem might be that you are trying to access your Firebase class through props when it needs to be instantiated in your Adminpanel component.
In your imports you have:
import { withFirebase } from '../Firebase'
Which looks as though you are trying to use an HOC but looking at Firebase.js, there is no indication of you passing a component in, to then pass props to.
Instead, in your Adminpanel component, try changing your import to:
import Firebase from '../Firebase'
Then, in the constructor of Adminpanel:
constructor(props) {
super(props);
this.state = {
loading:false,
posts:[]
}
this.firebase = new Firebase()
}
Then, try to call the class method by doing:
this.firebase.posts().on('value', snapshot => {
I am following this tutorial to create custom actions in React-admin (former Admin-on-rest): using a custom action creator.
However, after implement it, my code is not doing anything, i.e., the backend is not called.
I guess it is missing in the documentation a way to link the action with the dataProvider, unless the redux is handling it automagically.
Is it right? No need to link to the dataProvider being the opposite from what was made in the fetch example?
Following pieces of my code:
UpdatePage is pretty similar to ApproveButton in the tutorial:
// Update Page.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { updatePage } from '../../actions/pages';
import Button from '#material-ui/core/Button';
class UpdatePage extends Component {
handleClick = () => {
const { record } = this.props;
updatePage(record.id, record);
}
render() {
return <Button disabled={this.props.disabled} onClick={this.handleClick}>Confirmar</Button>;
}
}
UpdatePage.propTypes = {
disabled: PropTypes.bool.isRequired,
updatePage: PropTypes.func.isRequired,
record: PropTypes.object,
};
export default connect(null, {
updatePage
})(UpdatePage);`
Actions for the UpdatePage (like in the commentActions of the tutorial):
//in ../../actions/pages.js
import { UPDATE } from 'react-admin';
export const UPDATE_PAGE = 'UPDATE_PAGE';
export const updatePage = (id, data) => ({
type: UPDATE_PAGE,
payload: { id, data: { ...data, is_updated: true } },
meta: { fetch: UPDATE, resource: 'pages' },
});
You're invoking the action creator directly in the handleClick method. You need to use the updatePage function inside the props to properly dispatch the redux action.
handleClick = () => {
const { record } = this.props;
this.props.updatePage(record.id, record);
}
I have a connected container like so:
import React from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { autobind } from 'core-decorators';
const propTypes = {
data: PropTypes.object,
userData: PropTypes.object,
};
class ConnectedContainer extends React.Component {
#autobind
doSomethingImportant() {
...
}
render() {
....
}
}
ConnectedContainer.propTypes = propTypes;
const mapStateToProps = state => ({ data, userData });
const mapDispatchToProps = (dispatch) => {
return { actions: bindActionCreators(actions, dispatch) };
};
export default connect(mapStateToProps, mapDispatchToProps)(ConnectedContainer);
I want to test doSomethingImportant, so I have test like so:
import React from 'react';
import { shallow } from 'enzyme';
import ConnectedContainer from '.....'';
import configureStore from 'redux-mock-store';
const mockStore = configureStore();
const store = mockStore({ getState: () => null, dispatch: () => null, data: { data from fixture }, userData: { data from fixture } });
const container = (
<ConnectedContainer
store={store}
actions={{}}
/>
);
describe('ConnectedContainer', () => {
describe('doSomethingImportant', () => {
it('returns something important', () => {
const wrapper = shallow(container);
expect(wrapper.instance().doSomethingImportant()).to.equal( ... some output here );
});
});
});
And no matter what I do, I get this error:
TypeError: wrapper.instance(...). doSomethingImportant is not a function
What is happening with my container that I'm unable to access its instance methods?
Test the unwrapped component
Export the ConnectedContainer class itself, not the wrapped version. You want to test your code, not the connect() function.
You can keep the default export as the wrapped component, and then add the word export in front of the class definition:
export class ConnectedContainer extends React.Component { // Named export, for testing purposes only
...
}
Then in your test, import { ConnectedContainer } from '....'
Render that with shallow instead of the default export.
Naming conventions
Also, naming your component ConnectedContainer is very confusing! It only becomes connected after it is wrapped with the connect function. So when you export ConnectedContainer (as I've suggested) you're actually exporting a non-connected component class. (And is it actually a container? That's also pretty vague.)
One naming convention people use is to define a constant that holds the return value of connect(), and name that xxxContainer, like so:
export class IconView extends React.Component { // Exported for testing
// ... class implementation
}
function mapStateToProps(){...}
function mapDispatchToProps(){...}
const IconViewContainer = connect(mapStateToProps, mapDispatchToProps)(IconView);
export default IconViewContainer; // Exported for use as a component in other views
Try:
const Container = (
<ConnectedContainer
store={store}
actions={{}}
/>
);
Note, the only difference is the C uppercase in container. The thing is that React only treats classes and methods as components if they start with a capital letter. That could be your problem.
I connected redux to Next.js app just like in the docs (not sure what mapDispatchToProps does in the example though):
Init store method:
import { createStore, applyMiddleware } from 'redux';
import { createLogger } from 'redux-logger';
import axios from 'axios';
import axiosMiddleware from 'redux-axios-middleware';
import tokenMiddleware from './tokenMiddleware';
import getReducer from './combineReducers';
const logger = createLogger({ collapsed: true, diff: true });
const axiosMw = axiosMiddleware(axios.create(), { successSuffix: '_SUCCESS', errorSuffix: '_FAILURE' });
export default function initStore(logActions) {
return function init() {
const middleware = [tokenMiddleware, axiosMw];
if (logActions) middleware.push(logger);
return createStore(getReducer(), applyMiddleware(...middleware));
};
}
HOC which I use to connect pages:
import 'isomorphic-fetch';
import React from 'react';
import withRedux from 'next-redux-wrapper';
import { setUser } from 'lib/publisher/redux/actions/userActions';
import PublisherApp from './PublisherApp';
import initStore from '../redux/initStore';
export default Component => withRedux(initStore(), state => ({ state }))(
class extends React.Component {
static async getInitialProps({ store, isServer, req }) {
const cookies = req ? req.cookies : null;
if (cookies && cookies.user) {
store.dispatch(setUser(cookies.user));
}
return { isServer };
}
render() {
console.log(this.props.state);
return (
<PublisherApp {...this.props}>
<Component {...this.props} />
</PublisherApp>
);
}
}
);
The problem I'm having is that dispatched action
store.dispatch(setUser(cookies.user));
seems to work fine on server (I've debugged reducer and I know this user object from cookies is indeed handled by reducer) but when I do console.log(this.props.state) I get reducer with initial state - without user data.
You are missing second parameter inside createStore call. Try this:
export default function initStore(logActions) {
return function init(initData) {
const middleware = [tokenMiddleware, axiosMw];
if (logActions) middleware.push(logger);
return createStore(getReducer(), initData, applyMiddleware(...middleware));
};
}
Notice added initData parameter and it's usage.