title | author | image | tags | |||||
---|---|---|---|---|---|---|---|---|
Starting out with sensenet using Reactjs and Redux |
herflis |
../img/posts/sensenetecm-react-redux.png |
|
sensenet's new version is full of possibilities. One of them is the capability of being the base of your single-page-application with its Content Repository. If you are familiar with Reactjs, Redux or Aureliajs, sensenet can offer you even more with its toolkits, prebuilt control packs and many many more. In the following post we try to guide you with a step-by-step tutorial creating your first basic single page app with sensenet, Reactjs and Redux.
First of all you need a sensenet instance installed. The following project will get and set data from the Content Repository through OData REST API so it would be enough to install sensenet Services, but since sensenet 7 has not got its own admin surface yet probably your life will be easier if you install sensenet Webpages too. With sn-webpages you can access the good old Content Explorer with the control over all the users, permissions, settings, content types and many more.
Maybe you are already familiar with create-react-app which is an awesome tool that helps you creating React apps without ANY build configuration. Since we at sensenet write our code in TypeScript we use the forked TypeScript version create-react-app-typescript of it but it's not mandatory of course. To have a running basic React application, all you have to do is:
npm install -g create-react-app
create-react-app my-sn-app --scripts-version=react-scripts-ts
cd my-sn-app/
npm start
And voila, your app is running in the browser :)
To let your app communicate with the sensenet instance you have to allow its domain as the origin of CORS requests. The easiest way to do this if you open the Content Explorer and add your apps url to the AllowedOriginDomains
list in the Portal settings file (/Root/System/Settings/Portal.settings)
...
"AllowedOriginDomains": [
"localhost",
...
]
...
sensenet 7 has a JavaScript library that lets you work with its Content Repository by providing client API for the main content operations. We will create the base of our application by creating a client repository with sn-client-js
npm install --save-dev sn-client-js
If you are interested more in client repostory and how to take advantage of sn-client-js check its API references.
Import it into your React application's index.tsx
as a dependency and create a new SnRepository
with the url of your sensenet instance
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import { Repository } from 'sn-client-js';
import './index.css';
import 'rxjs';
const repository = new Repository.SnRepository({
RepositoryUrl: 'https://mysensenetsite.com'
});
ReactDOM.render(
<App />,
document.getElementById('root') as HTMLElement
);
registerServiceWorker();
To manage our application's state we use Redux and to make your life even more easy to work on this stack, we've created sn-redux that is a set of redux actions, reducers and redux-ovbservable epics for sensenet. In sn-redux we've implemented the main sensenet content operations so all you have to do is dispatch them and get everything from the state tree through predefined reducers. If you're interested - and why would you not - how sensenet's redux store is built up, check Diving deeper into sensenet's Redux store.
To create and configure your application's state container:
npm i --save-dev sn-redux sn-client-auth-google
import { combineReducers } from 'redux';
import { Store, Actions, Reducers } from 'sn-redux';
const sensenet = Reducers.sensenet;
const myReducer = combineReducers({
sensenet
});
const store = Store.configureStore(myReducer, undefined, undefined, {}, repository);
store.dispatch(Actions.InitSensenetStore('/Root/Sites/Default_Site', { select: 'all' }));
There're some configuration changes that should be made in the
tsconfig.json
. First add the following intto the compilereOptions"types": ["jest","node"]
. This is needed because create-react-app uses jest for testing but some of the dependencies like sn-redux using mocha, both have a variable nameddescribe
and it causes a conflict in the build process. Another thing to change is to setnoImplicitAny
andstrictNullChecks
to false in the tsconfig.json and setno-any
to false intslin.json
.
Now if you restart the application and check the dev toolbar you can see, that the Redux store of your application is initialized and some basic actions are already dispatched on it. We built redux-logger in sn-redux, so you can check the state tree in depth before and after every action.
Install react-redux and import it into your index.tsx
npm install --save react-redux
And wrap your app in a provider with your newly created store.
...
import { Provider } from 'react-redux';
...
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root') as HTMLElement
);
...
Now you are able to dispatch the predefined actions on the store, and connecting the store to your Reactjs components you can subscribe to the changes, use and display anything that can be found in the state tree.
See the available actions in the sn-redux API references.
List.tsx
import * as React from 'react';
import { connect } from 'react-redux';
import { Actions, Reducers } from 'sn-redux';
interface ListProps {
logout: Function;
fetch: Function;
children: Object;
ids: number[];
}
class List extends React.Component<ListProps, {}> {
constructor(props: any) {
super(props);
this.handleLogoutClick = this.handleLogoutClick.bind(this);
}
componentDidMount() {
this.props.fetch('/Root/Sites/Default_Site');
}
componentDidUpdate(prevOps: any) {
if (Object.keys(prevOps.ids).length !== Object.keys(this.props.ids).length) {
this.props.fetch('/Root/Sites/Default_Site');
}
}
handleLogoutClick(e: any) {
e.preventDefault();
this.props.logout();
}
render() {
const { children, ids } = this.props;
return (
<div>
<h1>Contentlist</h1>
{ids.length === 0 ? 'Empty list' :
<ul>
{ids.map(id => {
const content = children[id];
return <li key={content.Id}>{content.DisplayName}</li>;
})}
</ul>
}
<button onClick={e => this.handleLogoutClick(e)} > logout</button>
</div>
);
}
}
const mapStateToProps = (state, match) => {
return {
ids: Reducers.getIds(state.sensenet.children),
children: Reducers.getChildren(state.sensenet.children)
};
};
export default connect(mapStateToProps, {
logout: Actions.UserLogout,
fetch: Actions.RequestContent
})(List);
As you can see the component is connected to the Redux store. This way we are able to dispatch the actions listed above (UserLogout
and RequestContent
) and get some properties (fetched child items and the array containing their ids) from the state tree through Reducers predefined in sn-redux.
Login.tsx
import * as React from 'react';
export const Login = ({ formSubmit }) => {
let nameInput, passwordInput;
const onSubmit = e => {
e.preventDefault();
formSubmit(e, nameInput.value, passwordInput.value);
};
return (
<div>
<h1>Login</h1>
<form onSubmit={e => { onSubmit(e); }}>
<input type="text" placeholder="loginname" ref={el => nameInput = el} />
<input type="password" placeholder="password" ref={el => passwordInput = el} />
<button>login</button>
</form>
</div>
);
};
The login component is a simple stateless component. It is a tiny form with two input fields to let the user give her name and password. After submit it passes the name and password to a function defined in the parent component which will dispatch the login action.
App.tsx
Install react-router-dom and import it into your App.tsx
npm install --save react-router-dom
import * as React from 'react';
import {
Route,
Redirect,
withRouter
} from 'react-router-dom';
import { connect } from 'react-redux';
import { Authentication } from 'sn-client-js';
import { Actions, Reducers } from 'sn-redux';
import './App.css';
import List from './List';
import { Login } from './Login';
const styles = {
container: {
margin: '20px auto',
maxWidth: '50%'
}
};
interface AppProps {
loginState: Authentication.LoginState;
login: Function;
}
class App extends React.Component<AppProps, {}> {
constructor(props: any) {
super(props);
this.formSubmit = this.formSubmit.bind(this);
}
formSubmit(e: Event, email: string, password: string) {
this.props.login(email, password);
}
render() {
return (
<div style={styles.container}>
<Route
exact={true}
path="/"
render={routerProps => {
const status = this.props.loginState !== Authentication.LoginState.Authenticated;
return status ?
<Redirect key="login" to="/login" />
: <List />;
}}
/>
<Route
path="/login"
render={routerProps => {
const status = this.props.loginState !== Authentication.LoginState.Authenticated;
return status ?
<Login formSubmit={this.formSubmit} />
: <Redirect key="dashboard" to="/" />;
}}
/>
</div>
);
}
}
const mapStateToProps = (state, match) => {
return {
loginState: Reducers.getAuthenticationStatus(state.sensenet)
};
};
export default withRouter(connect(
mapStateToProps,
{
login: Actions.UserLogin,
})(App));
The App component is completed with routing to handle the redirect to the content list when the user is logged in and to the login form if she is not. This component is also connected to the store to get the user's current login state - which helps the app where it should be redirected - and to get the login action itself.
index.tsx
...
import {
HashRouter as Router
} from 'react-router-dom';
...
ReactDOM.render(
<Provider store={store}>
<Router basename="/">
<App store={store} />
</Router>
</Provider>,
document.getElementById('root') as HTMLElement
);
...
After restarting the project you can see that you have a not too stylish but working application. It has login and logout functionality and it lists some fetched content from the repository. The rest is up to you! :) The above mentioned stuff is only the tip of the iceberg.
If you are interested in more examples, we've created several todo apps not only with Reactjs, but with Angularjs, Aureliajs and Vuejs too, and we are currently working on a DMS MVP with Reactjs and Redux upon sensenet.
We are open and want to build a great community around sensenet so your opinion is super important to us. Please try, review and comment our work and help us make the world a better place! 😻